diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..d13785f5ce --- /dev/null +++ b/.editorconfig @@ -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 2016-Present Datadog, Inc. + +[*.{kt,kts}] +ktlint_code_style = android_studio +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ +max_line_length = 120 + +# Code generated by KotlinPoet +[buildSrc/src/test/kotlin/com/example/model/*.kt] +ktlint_standard = disabled + +# SPDX License Names +[buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/SPDXLicense.kt] +ktlint_standard_enum-entry-name-case = disabled \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4d69c0ccd9..ecd8c1aada 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-android ## Docs -/docs/ @DataDog/documentation @DataDog/rum-mobile -*README.md @DataDog/documentation @DataDog/rum-mobile +/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..9ecf0b0f42 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BugReport.yml @@ -0,0 +1,95 @@ +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/android/#debugging) 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: Logcat logs + description: | + Please provide Logcat 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_latest_sdk + attributes: + label: Did you confirm if the latest SDK version fixes the bug? + options: + - 'Yes' + - 'No' + validations: + required: true + - type: input + id: kotlin_java_version + attributes: + label: Kotlin / Java version + - type: input + id: gradle_version + attributes: + label: Gradle / AGP version + - type: textarea + id: dependencies + attributes: + label: Other dependencies versions + description: | + Relevant third party dependency versions. + e.g. okhttp 4.11.0 + - 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. + validations: + required: false + - 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..60962462b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/CrashReport.yml @@ -0,0 +1,85 @@ +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_latest_sdk + attributes: + label: Does the crash manifest in the latest SDK version? + options: + - 'Yes' + - 'No' + validations: + required: true + - type: input + id: kotlin_java_version + attributes: + label: Kotlin / Java version + - type: input + id: gradle_version + attributes: + label: Gradle / AGP version + - type: textarea + id: dependencies + attributes: + label: Other dependencies versions + description: | + Relevant third party dependency versions. + e.g. okhttp 4.11.0 + - 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..83f7729da8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FeatureRequest.yml @@ -0,0 +1,31 @@ +name: Feature Request +description: Have an idea or need a new feature? Request it here. +labels: ["enhancement"] +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..3c04ee56b1 --- /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..074c58f740 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/SetupIssue.yml @@ -0,0 +1,63 @@ +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: ["setup"] +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/android/#debugging). + - 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: input + id: kotlin_java_version + attributes: + label: Kotlin / Java version + - type: input + id: gradle_version + attributes: + label: Gradle / AGP version + - type: textarea + id: dependencies + attributes: + label: Other dependencies versions + description: | + Relevant third party dependency versions. + e.g. okhttp 4.11.0 + - 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/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 12e31aa754..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -Thanks for taking the time for reporting an issue! - -**Describe what happened** -Include any error message or stack trace if available. - -**Steps to reproduce the issue:** - -**Describe what you expected:** - -**Additional context** - - Android OS version: - - Device Model: - - Datadog SDK version: - - Versions of any other relevant dependencies (OkHttp, …): - - Proguard configuration: - - Gradle Plugins: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 4398d88682..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Are you requesting automatic instrumentation for a framework or library? Please describe.** -- Framework or library name : [e.g. `Realm`, `RxJava`] -- Library type: [e.g. database] -- Library version: [e.g. 5.2] - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6afe48bc61..5df407e2c1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,6 +13,6 @@ Anything else we should know when reviewing? ### Review checklist (to be filled by reviewers) - [ ] Feature or bugfix MUST have appropriate tests (unit, integration, e2e) -- [ ] Make sure you discussed the feature or bugfix with the maintaining team in an Issue -- [ ] Make sure each commit and the PR mention the Issue number (cf the [CONTRIBUTING](CONTRIBUTING.md) doc) +- [ ] Make sure you discussed the feature or bugfix with the maintaining team in an Issue +- [ ] Make sure each commit and the PR mention the Issue number (cf the [CONTRIBUTING](../CONTRIBUTING.md) doc) diff --git a/.github/chainguard/self.gitlab.read.sts.yaml b/.github/chainguard/self.gitlab.read.sts.yaml new file mode 100644 index 0000000000..e5f84597b3 --- /dev/null +++ b/.github/chainguard/self.gitlab.read.sts.yaml @@ -0,0 +1,6 @@ +issuer: https://gitlab.ddbuild.io + +subject_pattern: "project_path:DataDog/dd-sdk-android:ref_type:branch:ref:.*" + +permissions: + contents: read diff --git a/.github/workflows/changelog-to-confluence.yaml b/.github/workflows/changelog-to-confluence.yaml new file mode 100644 index 0000000000..d4f38720b9 --- /dev/null +++ b/.github/workflows/changelog-to-confluence.yaml @@ -0,0 +1,31 @@ +name: Publish Changelog to Confluence +on: + pull_request: + branches: + - develop + paths: + - 'CHANGELOG.md' +permissions: + contents: read +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + + - name: Prepare Only Changelog + run: | + mkdir -p publish_folder + cp CHANGELOG.md publish_folder/android-sdk-changelog.md + echo "Publishing only CHANGELOG.md" + + - name: Publish Markdown to Confluence + uses: markdown-confluence/publish-action@7767a0a7f438bb1497ee7ffd7d3d685b81dfe700 # v5 + with: + confluenceBaseUrl: ${{ secrets.DATADOG_CONFLUENCE_BASE_URL }} + confluenceParentId: ${{ secrets.CONFLUENCE_PARENT_ID }} + atlassianUserName: ${{ secrets.CONFLUENCE_ROBOT_RUM_EMAIL }} + atlassianApiToken: ${{ secrets.CONFLUENCE_ROBOT_RUM_API_KEY }} + contentRoot: '.' + folderToPublish: 'publish_folder' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..71ab20f361 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,62 @@ +name: "CodeQL" + +on: + push: + branches: [ "**" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master", "develop", "release/**", "feature/**" ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Java 17 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 + with: + distribution: 'zulu' + java-version: 17 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + with: + gradle-version: 8.1.1 + + # Manually build the java bytecode + - name: Execute Gradle build + run: ./gradlew assembleLibrariesRelease + + # Perform the analysis + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..0596730d96 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,41 @@ +name: 'Automatically close stale issues' + +on: + schedule: + # Runs every day at 8:00 AM CET + - cron: '0 7 * * *' + workflow_dispatch: + +permissions: + issues: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: | + This issue has been automatically marked as stale because it has not had recent activity and has the `awaiting-response` label. + + It will be closed if no further activity occurs within 3 days. + + # The label that will be added to the issues when automatically marked as stale + stale-issue-label: 'stale' + # The label that will be added to the issues when automatically marked as stale + close-issue-label: 'automatically closed' + # Only target issues with 'awaiting-response' label + only-labels: 'awaiting response' + # Mark issues as stale after 14 days + days-before-issue-stale: 14 + # Close issues after 3 days of being marked stale + days-before-issue-close: 3 + # Automatically remove the stale label when the issues or the pull requests are updated + remove-stale-when-updated: true + # Specify the reason used when closing issues: `completed` or `not_planned` + close-issue-reason: completed + # Run the stale workflow as dry-run. + # No GitHub API calls that can alter the issues and pull requests will happen. + # Useful to debug or when you want to configure the stale workflow safely. + debug-only: false diff --git a/.gitignore b/.gitignore index 8488c55924..b7611139c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # IntelliJ *.iml -.idea/ +.idea/** # Android Studio 3+ .navigation/ @@ -18,8 +18,20 @@ build/ # Local configuration file (sdk path, etc) local.properties repo/ -config/ +config/* # MacOS garbage .DS_Store +# .log +*.log + +# Kotlin +.kotlin/ + +# dd-sdk-android tools +gh_token +sdk_classpath +detekt_classpath +**/verification-metadata.xml +!gradle/verification-metadata.xml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 414e6ff8d1..bc0e69d4d2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,248 +1,26 @@ -include: - - 'https://gitlab-templates.ddbuild.io/slack-notifier/v1/template.yml' - -# SETUP - variables: - CURRENT_CI_IMAGE: "1" - CI_IMAGE_DOCKER: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/dd-sdk-android:$CURRENT_CI_IMAGE + CURRENT_CI_IMAGE: "21" + CI_IMAGE_DOCKER: registry.ddbuild.io/ci/dd-sdk-android:$CURRENT_CI_IMAGE GIT_DEPTH: 5 - DD_AGENT_HOST: "$BUILDENV_HOST_IP" DD_SERVICE: "dd-sdk-android" DD_ENV_TESTS: "ci" - DD_INTEGRATION_JUNIT_5_ENABLED: "true" - -stages: - - ci-image - - analysis - - test - - publish - - notify - -# CI IMAGE - -ci-image: - stage: ci-image - when: manual - except: [ tags, schedules ] - tags: [ "runner:docker", "size:large" ] - image: 486234852809.dkr.ecr.us-east-1.amazonaws.com/docker:18.03.1 - script: - - docker build --tag $CI_IMAGE_DOCKER -f Dockerfile.gitlab . - - docker push $CI_IMAGE_DOCKER - -# STATIC ANALYSIS - -analysis:ktlint: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: analysis - timeout: 30m - script: - - git fetch --depth=1 origin master - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :ktlintCheckAll --stacktrace --no-daemon - -analysis:android-lint: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: analysis - timeout: 30m - script: - - git fetch --depth=1 origin master - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :lintCheckAll --stacktrace --no-daemon - -analysis:detekt: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: analysis - timeout: 30m - script: - - git fetch --depth=1 origin master - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :detektAll --stacktrace --no-daemon - -analysis:licenses: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: analysis - timeout: 30m - script: - - git fetch --depth=1 origin master - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :dd-sdk-android:checkThirdPartyLicences :dd-sdk-android-timber:checkThirdPartyLicences --stacktrace --no-daemon - -analysis:api-surface: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: analysis - timeout: 30m - script: - - git fetch --depth=1 origin master - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :dd-sdk-android:checkApiSurfaceChanges :dd-sdk-android-timber:checkApiSurfaceChanges --stacktrace --no-daemon - -# TESTS - -test:debug: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: test - timeout: 1h - script: - - git fetch --depth=1 origin master - - rm -rf ~/.gradle/daemon/ - - CODECOV_TOKEN=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.codecov-token --with-decryption --query "Parameter.Value" --out text) - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :unitTestDebug --stacktrace --no-daemon - - bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN + DD_CIVISIBILITY_ENABLED: "true" + DD_INSIDE_CI: "true" + DD_COMMON_AGENT_CONFIG: "dd.env=ci,dd.trace.enabled=false,dd.jmx.fetch.enabled=false" -test:release: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: test - timeout: 1h - script: - - git fetch --depth=1 origin master - - rm -rf ~/.gradle/daemon/ - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :unitTestRelease --stacktrace --no-daemon + KUBERNETES_MEMORY_REQUEST: "8Gi" + KUBERNETES_MEMORY_LIMIT: "13Gi" -test:tools: - tags: [ "runner:main", "size:large" ] - image: $CI_IMAGE_DOCKER - stage: test - timeout: 1h - script: - - git fetch --depth=1 origin master - - rm -rf ~/.gradle/daemon/ - - GRADLE_OPTS="-XX:MaxPermSize=512m -Xmx2560m" ./gradlew :unitTestTools --stacktrace --no-daemon + EMULATOR_NAME: "android_emulator" + ANDROID_ARCH: "arm64-v8a" + ANDROID_API: "36" + ANDROID_SDK_VERSION: "commandlinetools-mac-11076708_latest" -# PUBLISH ARTIFACTS ON BINTRAY - -publish:release: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android:bintrayUpload --stacktrace --no-daemon - -publish:release-coil: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-coil:bintrayUpload --stacktrace --no-daemon - -publish:release-fresco: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-fresco:bintrayUpload --stacktrace --no-daemon - -publish:release-glide: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-glide:bintrayUpload --stacktrace --no-daemon - -publish:release-ktx: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-ktx:bintrayUpload --stacktrace --no-daemon - -publish:release-ndk: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-ndk:bintrayUpload --stacktrace --no-daemon - -publish:release-rx: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-rx:bintrayUpload --stacktrace --no-daemon - -publish:release-sqldelight: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-sqldelight:bintrayUpload --stacktrace --no-daemon - -publish:release-timber: - tags: [ "runner:main", "size:large" ] - only: - - tags - image: $CI_IMAGE_DOCKER - stage: publish - timeout: 30m - script: - - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties - - git fetch --depth=1 origin master - - ./gradlew :dd-sdk-android-timber:bintrayUpload --stacktrace --no-daemon - -# SLACK NOTIFICATIONS - -notify:release: - extends: .slack-notifier-base - stage: notify - when: on_success - only: - - tags - script: - - BINTRAY_URL="https://bintray.com/datadog/datadog-maven/dd-sdk-android/$CI_COMMIT_TAG" - - 'MESSAGE_TEXT=":package: $CI_PROJECT_NAME $CI_COMMIT_TAG published on :bintray: $BINTRAY_URL"' - - postmessage "#mobile-rum" "$MESSAGE_TEXT" - -notify:failure: - extends: .slack-notifier-base - stage: notify - when: on_failure - only: - - tags - script: - - BUILD_URL="$CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID" - - 'MESSAGE_TEXT=":status_alert: $CI_PROJECT_NAME $CI_COMMIT_TAG publish pipeline <$BUILD_URL|$COMMIT_MESSAGE> failed."' - - postmessage "#mobile-rum" "$MESSAGE_TEXT" \ No newline at end of file +include: + - local: 'ci/pipelines/default-pipeline.yml' + rules: + - if: '$CI_PIPELINE_KEY == null' + - local: 'ci/pipelines/check-release-pipeline.yml' + rules: + - if: '$CI_PIPELINE_KEY == "check-release"' diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 8d849f668c..0000000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
- - -
-
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 0f7bc519db..0000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/CHANGELOG.md b/CHANGELOG.md index 199b52a97a..ba512de5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,1476 @@ +# 3.2.0 / 2025-10-13 + +* [FEATURE] Support Apollo GraphQL. See [#2845](https://github.com/DataDog/dd-sdk-android/pull/2845) +* [FEATURE] Trace: Account ID and User ID propagation support via `baggage` header. See [#2911](https://github.com/DataDog/dd-sdk-android/pull/2911) +* [FEATURE] TTID (Time To Initial Display) reporting support. See [#2921](https://github.com/DataDog/dd-sdk-android/pull/2921) +* [BUGFIX] Fix tags which were missing in Vital event reported for Feature Operations. See [#2928](https://github.com/DataDog/dd-sdk-android/pull/2928) +* [BUGFIX] Fix RUM `sessionId` which was missing for automatic HTTP tracing via `baggage` header. See [#2904](https://github.com/DataDog/dd-sdk-android/pull/2904) +* [IMPROVEMENT] Move session properties to `ddtags` over query parameters. See [#2866](https://github.com/DataDog/dd-sdk-android/pull/2866) +* [IMPROVEMENT] Trace: Support `baggage` header updates. See [#2881](https://github.com/DataDog/dd-sdk-android/pull/2881) +* [IMPROVEMENT] Mute some Compose Reflection telemetry errors in Session Replay. See [#2901](https://github.com/DataDog/dd-sdk-android/pull/2901) +* [IMPROVEMENT] RUM: Set initialized flag to false when stop `RumFeature`. See [#2903](https://github.com/DataDog/dd-sdk-android/pull/2903) +* [IMPROVEMENT] RUM: Update doc of `addViewLoadingTime`. See [#2909](https://github.com/DataDog/dd-sdk-android/pull/2909) +* [IMPROVEMENT] Minor code improvements. See [#2913](https://github.com/DataDog/dd-sdk-android/pull/2913) +* [IMPROVEMENT] Session Replay: Extend resource handling to support multiple MIME types. See [#2914](https://github.com/DataDog/dd-sdk-android/pull/2914) +* [MAINTENANCE] Add `libs.versions.toml` in `GenerateTransitiveDependenciesTask` as input. See [#2905](https://github.com/DataDog/dd-sdk-android/pull/2905) +* [MAINTENANCE] Move `TimeProvider` into `dd-sdk-android-internal`. See [#2906](https://github.com/DataDog/dd-sdk-android/pull/2906) +* [MAINTENANCE] Bump `dd-trace-java` version to `1.54.0`. See [#2916](https://github.com/DataDog/dd-sdk-android/pull/2916) +* [MAINTENANCE] Enhancing check last release script. See [#2927](https://github.com/DataDog/dd-sdk-android/pull/2927) + +# 3.1.0 / 2025-09-18 + +* [FEATURE] RUM: Feature Operations public API. See [#2814](https://github.com/DataDog/dd-sdk-android/pull/2814) +* [FEATURE] RUM: Feature Operations DTO schema integration. See [#2816](https://github.com/DataDog/dd-sdk-android/pull/2816) +* [FEATURE] RUM: Feature Operations user debug logs support. See [#2819](https://github.com/DataDog/dd-sdk-android/pull/2819) +* [FEATURE] RUM: Feature Operations usage telemetry support. See [#2818](https://github.com/DataDog/dd-sdk-android/pull/2818) +* [FEATURE] RUM: Feature Operations parameters validation support. See [#2835](https://github.com/DataDog/dd-sdk-android/pull/2835) +* [FEATURE] RUM: Feature Operations background processing support. See [#2887](https://github.com/DataDog/dd-sdk-android/pull/2887) +* [FEATURE] RUM: Feature Operations synthetics attribute support. See [#2888](https://github.com/DataDog/dd-sdk-android/pull/2888) +* [IMPROVEMENT] RUM: Feature Operations Fix RumVitalEvent serialization logic. See [#2828](https://github.com/DataDog/dd-sdk-android/pull/2828) +* [IMPROVEMENT] Refactor the vitals screen in the sample application. See [#2820](https://github.com/DataDog/dd-sdk-android/pull/2820) +* [IMPROVEMENT] Add feature operation block to the `VitalsFragment`. See [#2821](https://github.com/DataDog/dd-sdk-android/pull/2821) +* [IMPROVEMENT] RUM: Replace `RumVitalEvent` with `VitalEvent`. See [#2831](https://github.com/DataDog/dd-sdk-android/pull/2831) +* [IMPROVEMENT] RUM: Fix the feature context in the telemetry. See [#2857](https://github.com/DataDog/dd-sdk-android/pull/2857) +* [IMPROVEMENT] Use single source of truth for `os` and `device` properties definitions for `Logs` and `Traces`. See [#2859](https://github.com/DataDog/dd-sdk-android/pull/2859) +* [IMPROVEMENT] Safe serialization of `account.extraInfo`. See [#2862](https://github.com/DataDog/dd-sdk-android/pull/2862) +* [IMPROVEMENT] Update MIGRATION.MD. See [#2858](https://github.com/DataDog/dd-sdk-android/pull/2858) +* [IMPROVEMENT] Fix for `VitalEvent` date. See [#2875](https://github.com/DataDog/dd-sdk-android/pull/2875) +* [IMPROVEMENT] Make `AndroidMDrawableToColorMapper` and `AndroidQDrawableToColorMapper` internal. See [#2873](https://github.com/DataDog/dd-sdk-android/pull/2873) +* [IMPROVEMENT] Trace: Add `@JvmStatic` to kotlin objects for cleaner interop with Java language. See [#2876](https://github.com/DataDog/dd-sdk-android/pull/2876) +* [IMPROVEMENT] RUM: Check for result of `activity.intent`. See [#2877](https://github.com/DataDog/dd-sdk-android/pull/2877) +* [IMPROVEMENT] Add documentation to the public API members where missing. See [#2880](https://github.com/DataDog/dd-sdk-android/pull/2880) +* [MAINTENANCE] Change the order of executors shutdown in `drainAndShutdownExecutors` to fix flaky integration tests. See [#2884](https://github.com/DataDog/dd-sdk-android/pull/2884) +* [MAINTENANCE] Bump `dd-trace-java` version to `1.53.0`. See [#2860](https://github.com/DataDog/dd-sdk-android/pull/2860) +* [MAINTENANCE] Split detekt custom rules to handle code point limit. See [#2863](https://github.com/DataDog/dd-sdk-android/pull/2863) +* [MAINTENANCE] Update Node version. See [#2864](https://github.com/DataDog/dd-sdk-android/pull/2864) +* [MAINTENANCE] Fix link to `CONTRIBUTING` doc from PR template. See [#2869](https://github.com/DataDog/dd-sdk-android/pull/2869) +* [MAINTENANCE] Update `CONTRIBUTING` doc with new modules. See [#2870](https://github.com/DataDog/dd-sdk-android/pull/2870) +* [MAINTENANCE] Bump language and API versions to 1.8. See [#2865](https://github.com/DataDog/dd-sdk-android/pull/2865) + +# 3.0.0 / 2025-09-04 + +This is the first official production version of SDK v3 containing the new architecture for tracing feature. See the [migration guide](https://github.com/DataDog/dd-sdk-android/blob/develop/MIGRATION.MD) for details. + +* [FEATURE] RUM: Create view attributes update methods. See [#2655](https://github.com/DataDog/dd-sdk-android/pull/2655) +* [IMPROVEMENT] Trace: Using `100%` instead of `20%` for the default network tracing sampling rate. Using `SAMPLED` instead of `ALL` as the default `TraceContextInjection` strategy. See [#2542](https://github.com/DataDog/dd-sdk-android/pull/2542) +* [IMPROVEMENT] Trace: Using session consistent trace sampling. See [#2544](https://github.com/DataDog/dd-sdk-android/pull/2544) +* [IMPROVEMENT] Core: Resolve batch file only during the actual write call. See [#2619](https://github.com/DataDog/dd-sdk-android/pull/2619) +* [IMPROVEMENT] Remove `forceNewBatch` API. See [#2621](https://github.com/DataDog/dd-sdk-android/pull/2621) +* [IMPROVEMENT] Core: Resolve file orchestrator for write operation from `DatadogContext`. See [#2624](https://github.com/DataDog/dd-sdk-android/pull/2624) +* [IMPROVEMENT] Introduce event processing thread. See [#2631](https://github.com/DataDog/dd-sdk-android/pull/2631) +* [IMPROVEMENT] Core: Push context changes from public API to the context thread. See [#2635](https://github.com/DataDog/dd-sdk-android/pull/2635) +* [IMPROVEMENT] Core: Make `getDatadogContext` read on the context thread. See [#2645](https://github.com/DataDog/dd-sdk-android/pull/2645) +* [IMPROVEMENT] RUM: Update RUM feature context only after event processing completion. See [#2650](https://github.com/DataDog/dd-sdk-android/pull/2650) +* [IMPROVEMENT] RUM: Align attribute propagation mechanism. See [#2654](https://github.com/DataDog/dd-sdk-android/pull/2654) +* [IMPROVEMENT] Trace: Perform lazy capture of `DatadogContext` at the span creation. See [#2662](https://github.com/DataDog/dd-sdk-android/pull/2662) +* [IMPROVEMENT] Trace: Remove deprecated `DatadogInterceptor` constructors. See [#2665](https://github.com/DataDog/dd-sdk-android/pull/2665) +* [IMPROVEMENT] Read RUM context in Session Replay in non-blocking manner. See [#2666](https://github.com/DataDog/dd-sdk-android/pull/2666) +* [IMPROVEMENT] RUM: Propagate `has_replay` flag to `RumContext` object. See [#2668](https://github.com/DataDog/dd-sdk-android/pull/2668) +* [IMPROVEMENT] RUM: Remove deprecated `startResource`. See [#2671](https://github.com/DataDog/dd-sdk-android/pull/2671) +* [IMPROVEMENT] RUM: Remove deprecated `userInfo` methods. See [#2672](https://github.com/DataDog/dd-sdk-android/pull/2672) +* [IMPROVEMENT] Custom endpoint URL are taken as is. See [#2685](https://github.com/DataDog/dd-sdk-android/pull/2685) +* [IMPROVEMENT] Logs: Don't send fatal errors to Logs, only send them to RUM. See [#2675](https://github.com/DataDog/dd-sdk-android/pull/2675) +* [IMPROVEMENT] Process feature context on the context thread. See [#2704](https://github.com/DataDog/dd-sdk-android/pull/2704) +* [IMPROVEMENT] Read feature context only when explicitly requested by the caller. See [#2716](https://github.com/DataDog/dd-sdk-android/pull/2716) +* [IMPROVEMENT] Avoid polling for `RumContext` in `VitalReaderRunnable`. See [#2728](https://github.com/DataDog/dd-sdk-android/pull/2728) +* [IMPROVEMENT] Remove feature name argument from APIs to set/remove feature context update listener. See [#2729](https://github.com/DataDog/dd-sdk-android/pull/2729) +* [IMPROVEMENT] Core: Mark `CoreFeature` properties used to create `DatadogContext` as volatile. See [#2738](https://github.com/DataDog/dd-sdk-android/pull/2738) +* [IMPROVEMENT] Core: Handle the case when `DatadogContext` is requested when SDK is getting deinitialized. See [#2740](https://github.com/DataDog/dd-sdk-android/pull/2740) +* [IMPROVEMENT] Core: Monitor backpressure of context executor. See [#2745](https://github.com/DataDog/dd-sdk-android/pull/2745) +* [IMPROVEMENT] Core: Remove default value for the `addAccountExtraInfo` call. See [#2759](https://github.com/DataDog/dd-sdk-android/pull/2759) +* [IMPROVEMENT] RUM: Make attributes argument optional in the event-related methods of RUM monitor. See [#2760](https://github.com/DataDog/dd-sdk-android/pull/2760) +* [IMPROVEMENT] Trace: Remove OpenTracing dependencies . See [#2783](https://github.com/DataDog/dd-sdk-android/pull/2783) +* [IMPROVEMENT] Trace: Update OpenTelemetry version. See [#2786](https://github.com/DataDog/dd-sdk-android/pull/2786) +* [IMPROVEMENT] Trace: Isolate implementation layer from integration modules. See [#2773](https://github.com/DataDog/dd-sdk-android/pull/2773) +* [IMPROVEMENT] Properties referencing support. See [#2793](https://github.com/DataDog/dd-sdk-android/pull/2793) +* [IMPROVEMENT] Use common-schema for common object generation. See [#2794](https://github.com/DataDog/dd-sdk-android/pull/2794) +* [IMPROVEMENT] Move session properties to `ddtags` over query parameters. See [#2812](https://github.com/DataDog/dd-sdk-android/pull/2812) +* [IMPROVEMENT] Trace: `TracingInterceptor` migration from OpenTracing to internal implementation. See [#2708](https://github.com/DataDog/dd-sdk-android/pull/2708) +* [IMPROVEMENT] Update Session Replay batch max age to 5h. See [#2842](https://github.com/DataDog/dd-sdk-android/pull/2842) +* [IMPROVEMENT] Core: Fix `clearUserInfo` API by adapting it to the context queue. See [#2847](https://github.com/DataDog/dd-sdk-android/pull/2847) +* [MAINTENANCE] Fix flaky `DatadogRumMonitor` tests. See [#2663](https://github.com/DataDog/dd-sdk-android/pull/2663) +* [MAINTENANCE] Minor cleanup. See [#2669](https://github.com/DataDog/dd-sdk-android/pull/2669) +* [MAINTENANCE] Update sample to use non-null user id. See [#2682](https://github.com/DataDog/dd-sdk-android/pull/2682) +* [MAINTENANCE] Bump Kotlin version used to `2.0.21`. See [#2766](https://github.com/DataDog/dd-sdk-android/pull/2766) +* [MAINTENANCE] Bump `minSdk` version to 23. See [#2844](https://github.com/DataDog/dd-sdk-android/pull/2844) + +# 2.26.2 / 2025-10-09 + +* [IMPROVEMENT] Extend resource handling to support multiple MIME types in RN. See [#2914](https://github.com/DataDog/dd-sdk-android/pull/2914) + +# 2.26.1 / 2025-09-11 + +* [BUGFIX] RUM: Move session properties to `ddtags` over query parameters. See [#2812](https://github.com/DataDog/dd-sdk-android/pull/2812) + +# 2.26.0 / 2025-08-27 + +* [FEATURE] RUM: Add battery and display attributes. See [#2815](https://github.com/DataDog/dd-sdk-android/pull/2815) +* [FEATURE] RUM: Implement `Navigation 3` view tracking side effect. See [#2830](https://github.com/DataDog/dd-sdk-android/pull/2830) +* [FEATURE] RUM: Make `Navigation 3` tracking listen to lifecycle. See [#2832](https://github.com/DataDog/dd-sdk-android/pull/2832) +* [IMPROVEMENT] Session Replay: `DrawableUtils` performance improvement. See [#2808](https://github.com/DataDog/dd-sdk-android/pull/2808) +* [IMPROVEMENT] RUM: Make accessibility send only mutations. See [#2806](https://github.com/DataDog/dd-sdk-android/pull/2806) +* [IMPROVEMENT] RUM: Create `Navigation 3` demo screen in sample app. See [#2825](https://github.com/DataDog/dd-sdk-android/pull/2825) +* [IMPROVEMENT] Move `OkHttp` client initialization to background thread. See [#2829](https://github.com/DataDog/dd-sdk-android/pull/2829) +* [MAINTENANCE] Add Github action to close stale issues. See [#2826](https://github.com/DataDog/dd-sdk-android/pull/2826) +* [MAINTENANCE] Add a schedule for the stale issues Github action. See [#2827](https://github.com/DataDog/dd-sdk-android/pull/2827) +* [MAINTENANCE] Remove outdated experimental annotations. See [#2833](https://github.com/DataDog/dd-sdk-android/pull/2833) +* [MAINTENANCE] Remove generation of models from internal schemas for RUM. See [#2834](https://github.com/DataDog/dd-sdk-android/pull/2834) +* [MAINTENANCE] Some additional accessibility test coverage. See [#2823](https://github.com/DataDog/dd-sdk-android/pull/2823) + +# 2.25.0 / 2025-07-28 + +* [FEATURE] RUM: Collect `Locale` attributes. See [#2797](https://github.com/DataDog/dd-sdk-android/pull/2797) +* [FEATURE] RUM: Add accessibility attributes. See [#2787](https://github.com/DataDog/dd-sdk-android/pull/2787) +* [BUGFIX] Fix `WindowCallbackWrapper` NPE. See [#2800](https://github.com/DataDog/dd-sdk-android/pull/2800) +* [MAINTENANCE] Fix release checking script. See [#2799](https://github.com/DataDog/dd-sdk-android/pull/2799) +* [MAINTENANCE] Next dev iteration. See [#2789](https://github.com/DataDog/dd-sdk-android/pull/2789) +* [MAINTENANCE] Merge `release/2.24.0` branch into `develop` branch. See [#2792](https://github.com/DataDog/dd-sdk-android/pull/2792) +* [MAINTENANCE] Update RUM schema. See [#2795](https://github.com/DataDog/dd-sdk-android/pull/2795) + +# 2.24.0 / 2025-07-16 + +* [FEATURE] Add Clear User Info API. See [#2768](https://github.com/DataDog/dd-sdk-android/pull/2768) +* [BUGFIX] Shallow copy node wireframes before iterating in `NodeFlattener`. See [#2736](https://github.com/DataDog/dd-sdk-android/pull/2736) +* [BUGFIX] Fix Session Replay NPE when getting `TextView` padding. See [#2784](https://github.com/DataDog/dd-sdk-android/pull/2784) +* [IMPROVEMENT] Stop posting recorded data item from main thread. See [#2763](https://github.com/DataDog/dd-sdk-android/pull/2763) +* [IMPROVEMENT] Stop telemetry for Compose `CheckBox` and `RadioButton`. See [#2775](https://github.com/DataDog/dd-sdk-android/pull/2775) +* [IMPROVEMENT] Introduce `_RumInternalProxy.setRumSessionTypeOverride`. See [#2776](https://github.com/DataDog/dd-sdk-android/pull/2776) +* [IMPROVEMENT] Add `updateExternalRefreshRate` to internal RUM API. See [#2772](https://github.com/DataDog/dd-sdk-android/pull/2772) +* [MAINTENANCE] Next dev iteration. See [#2752](https://github.com/DataDog/dd-sdk-android/pull/2752) +* [MAINTENANCE] Merge `release/2.23.0` branch into `develop` branch. See [#2755](https://github.com/DataDog/dd-sdk-android/pull/2755) +* [MAINTENANCE] Remove non-existent Gitlab file reference. See [#2753](https://github.com/DataDog/dd-sdk-android/pull/2753) +* [MAINTENANCE] Remove workaround when checking `dd-sdk-android-benchmark-internal` is published. See [#2758](https://github.com/DataDog/dd-sdk-android/pull/2758) +* [MAINTENANCE] Add `DDOCTOSTS_ID_TOKEN` to `dogfood-app` and `dogfood-demo`. See [#2757](https://github.com/DataDog/dd-sdk-android/pull/2757) +* [MAINTENANCE] Update `CONTRIBUTING` doc with missing modules. See [#2762](https://github.com/DataDog/dd-sdk-android/pull/2762) +* [MAINTENANCE] Update base Docker image to `Jammy`. See [#2761](https://github.com/DataDog/dd-sdk-android/pull/2761) +* [MAINTENANCE] Update docker image used in `ci-image` job. See [#2764](https://github.com/DataDog/dd-sdk-android/pull/2764) +* [MAINTENANCE] Use Datadog Gradle Plugin version 1.18.0. See [#2769](https://github.com/DataDog/dd-sdk-android/pull/2769) +* [MAINTENANCE] Migrate publishing from OSSRH to Central Publisher portal. See [#2770](https://github.com/DataDog/dd-sdk-android/pull/2770) +* [MAINTENANCE] Close Sonatype staging repo after publishing. See [#2774](https://github.com/DataDog/dd-sdk-android/pull/2774) +* [MAINTENANCE] Run unit tests from samples folder and for `tools:benchmark` module. See [#2777](https://github.com/DataDog/dd-sdk-android/pull/2777) +* [MAINTENANCE] Update `gradle-dependency-license` plugin to version 0.4.0. See [#2788](https://github.com/DataDog/dd-sdk-android/pull/2788) + +# 2.23.0 / 2025-06-23 + +* [FEATURE] Global: Add public APIs for set account information. See [#2694](https://github.com/DataDog/dd-sdk-android/pull/2694) +* [FEATURE] Global: Add `AccountInfo` data class and provider. See [#2696](https://github.com/DataDog/dd-sdk-android/pull/2696) +* [FEATURE] Global: Add `AccountInfo` into `CoreFeature` and `DatadogContext`. See [#2702](https://github.com/DataDog/dd-sdk-android/pull/2702) +* [FEATURE] RUM: Add account information into RUM events. See [#2706](https://github.com/DataDog/dd-sdk-android/pull/2706) +* [FEATURE] Logs: Add Account Information in Logs. See [#2710](https://github.com/DataDog/dd-sdk-android/pull/2710) +* [FEATURE] Traces: Add account information in Trace. See [#2709](https://github.com/DataDog/dd-sdk-android/pull/2709) +* [FEATURE] Global: Add `AP2` datacenter support. See [#2734](https://github.com/DataDog/dd-sdk-android/pull/2734) +* [BUGFIX] RUM: Report resource with size 0. See [#2688](https://github.com/DataDog/dd-sdk-android/pull/2688) +* [BUGFIX] Session Replay: Fix `NullPointerException` in `isOnSecondaryDisplay` method. See [#2701](https://github.com/DataDog/dd-sdk-android/pull/2701) +* [BUGFIX] Session Replay: Defer drawable copy to work thread in Session Replay. See [#2723](https://github.com/DataDog/dd-sdk-android/pull/2723) +* [BUGFIX] RUM: Create a new `RumViewScope` when the session is renewed. See [#2699](https://github.com/DataDog/dd-sdk-android/pull/2699) +* [BUGFIX] RUM: Fix effective sample rate calculation for `SessionEndedMetricDispatcher`. See [#2744](https://github.com/DataDog/dd-sdk-android/pull/2744) +* [IMPROVEMENT] RUM: Allow 24h batch backlog for RUM. See [#2690](https://github.com/DataDog/dd-sdk-android/pull/2690) +* [IMPROVEMENT] RumAuto scenario for Android Benchmark app. See [#2689](https://github.com/DataDog/dd-sdk-android/pull/2689) +* [IMPROVEMENT] Fix Proguard rules for Method Called Metrics. See [#2691](https://github.com/DataDog/dd-sdk-android/pull/2691) +* [IMPROVEMENT] RUM: Enhancement of Compose Actions tracking reflection. See [#2692](https://github.com/DataDog/dd-sdk-android/pull/2692) +* [IMPROVEMENT] Add Account Info fragment for sample app. See [#2703](https://github.com/DataDog/dd-sdk-android/pull/2703) +* [IMPROVEMENT] Add Android TV sample app. See [#2697](https://github.com/DataDog/dd-sdk-android/pull/2697) +* [IMPROVEMENT] Session Replay: Remove `ContentPainterElement` Class Reflection from Telemetry. See [#2714](https://github.com/DataDog/dd-sdk-android/pull/2714) +* [IMPROVEMENT] Global: Update `setUserInfo` and `setAccountInfo` documentation. See [#2715](https://github.com/DataDog/dd-sdk-android/pull/2715) +* [IMPROVEMENT] Session Replay: Avoid copying hardware bitmap in Session Replay. See [#2732](https://github.com/DataDog/dd-sdk-android/pull/2732) +* [IMPROVEMENT] Add integration tests of set account information in `reliability` module. See [#2725](https://github.com/DataDog/dd-sdk-android/pull/2725) +* [IMPROVEMENT] Fix Jetpack Compose Auto instrumentation telemetry in benchmark app. See [#2737](https://github.com/DataDog/dd-sdk-android/pull/2737) +* [MAINTENANCE] Next dev iteration 2.23.0. See [#2678](https://github.com/DataDog/dd-sdk-android/pull/2678) +* [MAINTENANCE] Add BackPressured Dump information into Telemetry log. See [#2673](https://github.com/DataDog/dd-sdk-android/pull/2673) +* [MAINTENANCE] Change Benchmark artifact Id to `dd-sdk-android-benchmark`. See [#2686](https://github.com/DataDog/dd-sdk-android/pull/2686) +* [MAINTENANCE] Add Method Called Metric on Rum event handling. See [#2687](https://github.com/DataDog/dd-sdk-android/pull/2687) +* [MAINTENANCE] Bump Datadog Gradle Plugin to 1.17.0. See [#2693](https://github.com/DataDog/dd-sdk-android/pull/2693) +* [MAINTENANCE] Generate checksum xml. See [#2695](https://github.com/DataDog/dd-sdk-android/pull/2695) +* [MAINTENANCE] Generate `verification-metadata` with pgp information. See [#2698](https://github.com/DataDog/dd-sdk-android/pull/2698) +* [MAINTENANCE] Move `LocalAttribute` class and related extension methods to the `internal` module. See [#2705](https://github.com/DataDog/dd-sdk-android/pull/2705) +* [MAINTENANCE] Move public top-level extension functions and properties from `core` module to `internal` module. See [#2707](https://github.com/DataDog/dd-sdk-android/pull/2707) +* [MAINTENANCE] Remove `Project.exec` usage. See [#2674](https://github.com/DataDog/dd-sdk-android/pull/2674) +* [MAINTENANCE] Improve speed of running `core-it` suite. See [#2711](https://github.com/DataDog/dd-sdk-android/pull/2711) +* [MAINTENANCE] Fix `GsonExt` flaky test. See [#2719](https://github.com/DataDog/dd-sdk-android/pull/2719) +* [MAINTENANCE] Add Android Automotive sample. See [#2724](https://github.com/DataDog/dd-sdk-android/pull/2724) +* [MAINTENANCE] GitHub app migration for PAT. See [#2726](https://github.com/DataDog/dd-sdk-android/pull/2726) +* [MAINTENANCE] Fix `RumSessionScope` flaky test. See [#2730](https://github.com/DataDog/dd-sdk-android/pull/2730) +* [MAINTENANCE] Introduce check release pipeline. See [#2731](https://github.com/DataDog/dd-sdk-android/pull/2731) +* [MAINTENANCE] Remove unnecessary comment. See [#2735](https://github.com/DataDog/dd-sdk-android/pull/2735) +* [MAINTENANCE] Add `dd-octo-sts` to Dockerfile. See [#2739](https://github.com/DataDog/dd-sdk-android/pull/2739) +* [MAINTENANCE] Add `dd-octo-sts` policy for read access. See [#2741](https://github.com/DataDog/dd-sdk-android/pull/2741) +* [MAINTENANCE] Fix `dd-octo-sts` pattern. See [#2743](https://github.com/DataDog/dd-sdk-android/pull/2743) +* [MAINTENANCE] Use `dd-octo-sts` to check release. See [#2742](https://github.com/DataDog/dd-sdk-android/pull/2742) +* [MAINTENANCE] Use `dd-octo-sts` to create PRs in `dd-sdk-android-gradle-plugin`. See [#2746](https://github.com/DataDog/dd-sdk-android/pull/2746) +* [MAINTENANCE] Use `dd-octo-sts` to create PRs in Shopist and Mobile app. See [#2750](https://github.com/DataDog/dd-sdk-android/pull/2750) +* [DOCS] Update Publish Changelog to Confluence: Fix issue with already present page. See [#2683](https://github.com/DataDog/dd-sdk-android/pull/2683) +* [DOCS] Update `changelog-to-confluence`: Update secret names. See [#2727](https://github.com/DataDog/dd-sdk-android/pull/2727) + +# 2.22.0 / 2025-05-28 + +* [FEATURE] Session Replay: Support Coil3 for Session Replay image recording. See [#2648](https://github.com/DataDog/dd-sdk-android/pull/2648) +* [FEATURE] RUM: Add Compose custom attributes for actions tracking. See [#2661](https://github.com/DataDog/dd-sdk-android/pull/2661) +* [BUGFIX] RUM: Fix background session start reason. See [#2623](https://github.com/DataDog/dd-sdk-android/pull/2623) +* [BUGFIX] Core: Catch exceptions during the Power or Battery broadcast intents processing. See [#2642](https://github.com/DataDog/dd-sdk-android/pull/2642) +* [IMPROVEMENT] `LogsCustom` scenario for Android benchmark app. See [#2625](https://github.com/DataDog/dd-sdk-android/pull/2625) +* [IMPROVEMENT] `LogsHeavyTraffic` scenario for Android benchmarks. See [#2629](https://github.com/DataDog/dd-sdk-android/pull/2629) +* [IMPROVEMENT] `Trace` scenario for Android benchmark app. See [#2637](https://github.com/DataDog/dd-sdk-android/pull/2637) +* [IMPROVEMENT] Replace `addFirst` usage in `PendingTrace`. See [#2638](https://github.com/DataDog/dd-sdk-android/pull/2638) +* [IMPROVEMENT] `RumManual` scenario for Android benchmark app. See [#2644](https://github.com/DataDog/dd-sdk-android/pull/2644) +* [IMPROVEMENT] Align Datadog initialization with iOS for benchmark app. See [#2647](https://github.com/DataDog/dd-sdk-android/pull/2647) +* [IMPROVEMENT] Fix Datadog core creation in baseline benchmark run. See [#2649](https://github.com/DataDog/dd-sdk-android/pull/2649) +* [IMPROVEMENT] RUM: Add Jetpack Compose specific log in gesture listener if target not found. See [#2651](https://github.com/DataDog/dd-sdk-android/pull/2651) +* [IMPROVEMENT] Add sample screen for Compose image content scale. See [#2659](https://github.com/DataDog/dd-sdk-android/pull/2659) +* [IMPROVEMENT] Remove `addFirst`, `removeFirst`, `removeLast` usages. See [#2664](https://github.com/DataDog/dd-sdk-android/pull/2664) +* [IMPROVEMENT] RUM: Use reflection to retrieve semantics information in modifier. See [#2667](https://github.com/DataDog/dd-sdk-android/pull/2667) +* [MAINTENANCE] Next dev iteration. See [#2630](https://github.com/DataDog/dd-sdk-android/pull/2630) +* [MAINTENANCE] Merge `release/2.21.0` into develop. See [#2632](https://github.com/DataDog/dd-sdk-android/pull/2632) +* [MAINTENANCE] Update `unMock` plugin version, remove `me.xdrop:fuzzywuzzy` dependency, remove deprecated Gradle APIs. See [#2634](https://github.com/DataDog/dd-sdk-android/pull/2634) +* [MAINTENANCE] Speed up some tests in `FeatureScopeTest` by reducing unconditional wait time. See [#2643](https://github.com/DataDog/dd-sdk-android/pull/2643) +* [MAINTENANCE] Fix logging format for error messages in `PlainBatchFileReaderWriter`. See [#2652](https://github.com/DataDog/dd-sdk-android/pull/2652) +* [MAINTENANCE] Remove telemetry target from error logging in `PlainBatchFileReaderWriter`. See [#2653](https://github.com/DataDog/dd-sdk-android/pull/2653) +* [MAINTENANCE] Better project-type dependency resolution for Detekt custom rules execution. See [#2670](https://github.com/DataDog/dd-sdk-android/pull/2670) + +# 2.21.0 / 2025-05-06 + +* [FEATURE] Add Compose instrumentation API for view tracking and image recording. See [#2570](https://github.com/DataDog/dd-sdk-android/pull/2570) +* [FEATURE] Add `ActionTrackingStrategy` interface to decouple find view logic. See [#2573](https://github.com/DataDog/dd-sdk-android/pull/2573) +* [FEATURE] Add Compose actions tracking strategy API in `RumConfiguration`. See [#2575](https://github.com/DataDog/dd-sdk-android/pull/2575) +* [FEATURE] Implement Compose actions tracking strategy. See [#2586](https://github.com/DataDog/dd-sdk-android/pull/2586) +* [FEATURE] Change `TrackViews` and `RecordImages` annotations to `ComposeInstrumentation`. See [#2595](https://github.com/DataDog/dd-sdk-android/pull/2595) +* [BUGFIX] Fix Rum Action Tap is added twice for every `ACTION_UP`. See [#2579](https://github.com/DataDog/dd-sdk-android/pull/2579) +* [BUGFIX] Cover `IndexOutOfBoundsException` in `DrawableUtils`. See [#2604](https://github.com/DataDog/dd-sdk-android/pull/2604) +* [BUGFIX] Let `AndroidTracer.logErrorMessage()` send an ERROR log. See [#2605](https://github.com/DataDog/dd-sdk-android/pull/2605) +* [BUGFIX] Fix artifacts in Jetpack Compose scrolling. See [#2610](https://github.com/DataDog/dd-sdk-android/pull/2610) +* [BUGFIX] Fix `ViewTarget` is garbage collected during actions tracking. See [#2608](https://github.com/DataDog/dd-sdk-android/pull/2608) +* [BUGFIX] Fix the memory leak in the `PendingTrace#cleaner`. See [#2607](https://github.com/DataDog/dd-sdk-android/pull/2607) +* [BUGFIX] Fix Session Replay is not resumed after the session has expired before. See [#2611](https://github.com/DataDog/dd-sdk-android/pull/2611) +* [BUGFIX] Catch NPE when drawing cloned drawable in Session Replay. See [#2622](https://github.com/DataDog/dd-sdk-android/pull/2622) +* [BUGFIX] Revert: Remove shared `ThreadLocal` scopes. See [#2603](https://github.com/DataDog/dd-sdk-android/pull/2603) +* [IMPROVEMENT] Remove test fixtures content root duplication in `dd-sdk-android-session-replay-compose` module. See [#2592](https://github.com/DataDog/dd-sdk-android/pull/2592) +* [IMPROVEMENT] Minor improvements. See [#2609](https://github.com/DataDog/dd-sdk-android/pull/2609) +* [IMPROVEMENT] Propagate session ID in baggage header. See [#2602](https://github.com/DataDog/dd-sdk-android/pull/2602) +* [MAINTENANCE] Add workflow: Changelog update to Confluence. See [#2596](https://github.com/DataDog/dd-sdk-android/pull/2596) +* [MAINTENANCE] Add telemetry for Compose instrumentation functions. See [#2601](https://github.com/DataDog/dd-sdk-android/pull/2601) +* [MAINTENANCE] Add Android Auto and Android XR support to the sample app. See [#2606](https://github.com/DataDog/dd-sdk-android/pull/2606) +* [MAINTENANCE] Add metrics for internal benchmarking. See [#2581](https://github.com/DataDog/dd-sdk-android/pull/2581) +* [MAINTENANCE] Sample telemetry for `RecordedDataQueueHandler`. See [#2600](https://github.com/DataDog/dd-sdk-android/pull/2600) +* [MAINTENANCE] Remove the noisy warning log as for some views it is normal to not have ITV. See [#2617](https://github.com/DataDog/dd-sdk-android/pull/2617) +* [MAINTENANCE] Fix semantics of `ExecutorService.submit` vs `Executor.execute` usage. See [#2616](https://github.com/DataDog/dd-sdk-android/pull/2616) +* [MAINTENANCE] Integrate Datadog Plugin in benchmark application. See [#2618](https://github.com/DataDog/dd-sdk-android/pull/2618) +* [MAINTENANCE] Merge `feature/compose-instrumentation-api` into `feature/actions-tracking`. See [#2583](https://github.com/DataDog/dd-sdk-android/pull/2583) +* [MAINTENANCE] Merge 2.20.0 into `develop`. See [#2588](https://github.com/DataDog/dd-sdk-android/pull/2588) +* [MAINTENANCE] Merge `Feature/actions-tracking` into `develop`. See [#2598](https://github.com/DataDog/dd-sdk-android/pull/2598) +* [MAINTENANCE] Bump version to 2.21.0-SNAPSHOT. See [#2585](https://github.com/DataDog/dd-sdk-android/pull/2585) +* [MAINTENANCE] Bump `targetSdk` to 36. See [#2589](https://github.com/DataDog/dd-sdk-android/pull/2589) + +# 2.20.0 / 2025-04-07 + +* [FEATURE] Slow frames collection support. See [#2518](https://github.com/DataDog/dd-sdk-android/pull/2518) +* [FEATURE] Introduce `UISlownessMetricDispatcher`. See [#2567](https://github.com/DataDog/dd-sdk-android/pull/2567) +* [BUGFIX] Remove double computation of the `RUM` payload. See [#2528](https://github.com/DataDog/dd-sdk-android/pull/2528) +* [BUGFIX] Add try-catch in `drawOnCanvas` in order to catch exceptions from `draw(canvas)` method. See [#2549](https://github.com/DataDog/dd-sdk-android/pull/2549) +* [BUGFIX] Fix issue with missing freeze rate and slow frames rate. See [#2557](https://github.com/DataDog/dd-sdk-android/pull/2557) +* [BUGFIX] Ignore secondary displays in `Session Replay`. See [#2574](https://github.com/DataDog/dd-sdk-android/pull/2574) +* [IMPROVEMENT] Set the `local-ci` script to check for specific version of KtLint. See [#2526](https://github.com/DataDog/dd-sdk-android/pull/2526) +* [IMPROVEMENT] Add missing builder function for anonymous user tracking. See [#2540](https://github.com/DataDog/dd-sdk-android/pull/2540) +* [IMPROVEMENT] Add telemetry for pending batch files. See [#2548](https://github.com/DataDog/dd-sdk-android/pull/2548) +* [IMPROVEMENT] Remove the possibility to read the `Tracer` config from `env` and from the config file. See [#2564](https://github.com/DataDog/dd-sdk-android/pull/2564) +* [IMPROVEMENT] Add `traceSampleRate` to the telemetry `Configuration` events. See [#2563](https://github.com/DataDog/dd-sdk-android/pull/2563) +* [IMPROVEMENT] Report configured distributed tracing headers as part of `Configuration` telemetry. See [#2572](https://github.com/DataDog/dd-sdk-android/pull/2572) +* [IMPROVEMENT] Optimize features context reads in `TelemetryEventHandler`. See [#2576](https://github.com/DataDog/dd-sdk-android/pull/2576) +* [IMPROVEMENT] Optimize `OkHttp` configuration telemetry. See [#2578](https://github.com/DataDog/dd-sdk-android/pull/2578) +* [IMPROVEMENT] Make SDK support 16Kb page sizes. See [#2580](https://github.com/DataDog/dd-sdk-android/pull/2580) +* [MAINTENANCE] Update AndroidX `Metrics` library to 1.0.0-beta02. See [#2546](https://github.com/DataDog/dd-sdk-android/pull/2546) +* [MAINTENANCE] Remove `RUM` feature check in `AndroidTracer` builder. [#2539](https://github.com/DataDog/dd-sdk-android/pull/2539) +* [MAINTENANCE] Change `TraceWriter#write` type from `MutableList` to `List`. See [#2568](https://github.com/DataDog/dd-sdk-android/pull/2568) +* [MAINTENANCE] Fix flaky `TraceWriterTest` M log error and proceed W write() { serialization failed }. See [#2565](https://github.com/DataDog/dd-sdk-android/pull/2565) +* [MAINTENANCE] Fix flaky tests in `OtelTraceWriter`. See [#2571](https://github.com/DataDog/dd-sdk-android/pull/2571) +* [MAINTENANCE] Change `OtelTraceWriter#write type` from `MutableList` to `List`. See [#2577](https://github.com/DataDog/dd-sdk-android/pull/2577) +* [MAINTENANCE] Fix negative values in slow frames, adjusting telemetry. See [#2582](https://github.com/DataDog/dd-sdk-android/pull/2582) + +# 2.19.2 / 2025-03-20 + +* [BUGFIX] Fix NPE with Metrics listener on older APIs. See[#2558](https://github.com/DataDog/dd-sdk-android/pull/2558) + +# 2.19.1 / 2025-03-17 + +* [BUGFIX] Fix NPE when `Drawable.getCurrent` returns null. See[#2545](https://github.com/DataDog/dd-sdk-android/pull/2545) + +# 2.19.0 / 2025-03-10 + +* [FEATURE] Core: Introduce anonymous RUM Identifier. See [#2487](https://github.com/DataDog/dd-sdk-android/pull/2487) +* [BUGFIX] Fixing telemetry sampling rate reporting. See [#2503](https://github.com/DataDog/dd-sdk-android/pull/2503) +* [BUGFIX] Allow first build complete to be any number format. See [#2527](https://github.com/DataDog/dd-sdk-android/pull/2527) +* [IMPROVEMENT] RUM View Ended Telemetry now includes TNS and INV. See [#2495](https://github.com/DataDog/dd-sdk-android/pull/2495) +* [IMPROVEMENT] Trace: Forward RUM Session ID in trace headers. See [#2502](https://github.com/DataDog/dd-sdk-android/pull/2502) +* [IMPROVEMENT] View ended instrumentation type attribute support. See [#2504](https://github.com/DataDog/dd-sdk-android/pull/2504) +* [IMPROVEMENT] Support for configuration schema updates for time based strategy of TNS and INV metrics. See [#2505](https://github.com/DataDog/dd-sdk-android/pull/2505) +* [IMPROVEMENT] Core: Change `UploadWorker` visibility from `internal` to `public`. See [#2511](https://github.com/DataDog/dd-sdk-android/pull/2511) +* [IMPROVEMENT] Ensure span logs use 128 bits trace id as hex string. See [#2512](https://github.com/DataDog/dd-sdk-android/pull/2512) +* [IMPROVEMENT] RUM: Refactor `JankStatsActivityLifecycleListener`. See [#2513](https://github.com/DataDog/dd-sdk-android/pull/2513) +* [IMPROVEMENT] Core: Update `UserInfo` API to make id mandatory. See [#2509](https://github.com/DataDog/dd-sdk-android/pull/2509) +* [IMPROVEMENT] Improvements to the upload mechanism. See [#2514](https://github.com/DataDog/dd-sdk-android/pull/2514) +* [IMPROVEMENT] Catch Coroutines errors while getting all threads stacktraces. See [#2522](https://github.com/DataDog/dd-sdk-android/pull/2522) +* [IMPROVEMENT] Support Flutter's FBC and custom INV values. See [#2520](https://github.com/DataDog/dd-sdk-android/pull/2520) +* [IMPROVEMENT] RUM: Add ability to manually add an activity to `JankStats`. See [#2524](https://github.com/DataDog/dd-sdk-android/pull/2524) +* [IMPROVEMENT] Session Replay: Allow definition of custom implementations of specific Session Replay methods. See [#2516](https://github.com/DataDog/dd-sdk-android/pull/2516) +* [IMPROVEMENT] Session Replay: Improve `StateListDrawable` support in session replay. See [#2531](https://github.com/DataDog/dd-sdk-android/pull/2531) +* [MAINTENANCE] Next dev iteration `2.19.0`. See [#2498](https://github.com/DataDog/dd-sdk-android/pull/2498) +* [MAINTENANCE] Fix flaky test in Head-based sampling test suite. See [#2499](https://github.com/DataDog/dd-sdk-android/pull/2499) +* [MAINTENANCE] Session Replay: Add support for Detekt checks for Session Replay Compose module. See [#2507](https://github.com/DataDog/dd-sdk-android/pull/2507) +* [MAINTENANCE] Update AGP version to `8.8.2`. See [#2515](https://github.com/DataDog/dd-sdk-android/pull/2515) +* [MAINTENANCE] RUM: Avoid logging initial `null` `viewLoadingTime` on first call to `addViewLoadingTime`. See [#2517](https://github.com/DataDog/dd-sdk-android/pull/2517) +* [MAINTENANCE] Upgrade github action to use `ubuntu-latest`. See [#2523](https://github.com/DataDog/dd-sdk-android/pull/2523) +* [DOCS] Update SDK performance doc with Session Replay measurements. See [#2481](https://github.com/DataDog/dd-sdk-android/pull/2481) + +# 2.18.0 / 2025-02-03 + +* [FEATURE] Allow disabling 404 span redaction. See [#2496](https://github.com/DataDog/dd-sdk-android/pull/2496) +* [IMPROVEMENT] Improve telemetry on invalid view duration. See [#2466](https://github.com/DataDog/dd-sdk-android/pull/2466) +* [IMPROVEMENT] Improve the view duration accuracy. See [#2467](https://github.com/DataDog/dd-sdk-android/pull/2467) +* [IMPROVEMENT] Fix internal telemetry on invalid loading time usage. See [#2468](https://github.com/DataDog/dd-sdk-android/pull/2468) +* [IMPROVEMENT] Add `Slider` semantics node mapper. See [#2459](https://github.com/DataDog/dd-sdk-android/pull/2459) +* [IMPROVEMENT] Fix ProGuard rules for Compose `Checkbox`. See [#2470](https://github.com/DataDog/dd-sdk-android/pull/2470) +* [IMPROVEMENT] Remove non-critical Compose reflection error from telemetry. See [#2476](https://github.com/DataDog/dd-sdk-android/pull/2476) +* [IMPROVEMENT] Add Semantics mapper for `Switch`. See [#2471](https://github.com/DataDog/dd-sdk-android/pull/2471) +* [IMPROVEMENT] Apply contrasting color to `Semantics` component. See [#2477](https://github.com/DataDog/dd-sdk-android/pull/2477) +* [IMPROVEMENT] Fix center crop image is not cropped in wireframe. See [#2479](https://github.com/DataDog/dd-sdk-android/pull/2479) +* [IMPROVEMENT] Add `RadioButton` color. See [#2478](https://github.com/DataDog/dd-sdk-android/pull/2478) +* [IMPROVEMENT] Update enums to match RUM event schema. See [#2482](https://github.com/DataDog/dd-sdk-android/pull/2482) +* [IMPROVEMENT] Fix `_dd.rule_psr` attribute calculation for RUM. See [#2485](https://github.com/DataDog/dd-sdk-android/pull/2485) +* [IMPROVEMENT] Implement Head-based sampling for network instrumentation. See [#2483](https://github.com/DataDog/dd-sdk-android/pull/2483) +* [IMPROVEMENT] Add OpenTracing API as exported dependency to OkHttp instrumentation. See [#2488](https://github.com/DataDog/dd-sdk-android/pull/2488) +* [IMPROVEMENT] Improve `updateFeatureContext` performances. See [#2489](https://github.com/DataDog/dd-sdk-android/pull/2489) +* [IMPROVEMENT] Adjust telemetry metrics sampling rates. See [#2490](https://github.com/DataDog/dd-sdk-android/pull/2490) +* [MAINTENANCE] Bump develop version to 2.18.0 snapshot. See [#2465](https://github.com/DataDog/dd-sdk-android/pull/2465) +* [MAINTENANCE] Update dependencies, adjusting detekt rules, fix tests. See [#2463](https://github.com/DataDog/dd-sdk-android/pull/2463) +* [MAINTENANCE] Merge release/2.17.0 into develop. See [#2473](https://github.com/DataDog/dd-sdk-android/pull/2473) +* [MAINTENANCE] Update AGP to version 8.7.3. See [#2484](https://github.com/DataDog/dd-sdk-android/pull/2484) +* [MAINTENANCE] Include Java API surface file in the API changes check. See [#2493](https://github.com/DataDog/dd-sdk-android/pull/2493) +* [MAINTENANCE] Update RUM Schema. See [#2492](https://github.com/DataDog/dd-sdk-android/pull/2492) +* [MAINTENANCE] Pin github actions to exact commit hash. See [#2494](https://github.com/DataDog/dd-sdk-android/pull/2494) +* [DOCS] Fix KDoc for `RumMonitor#stopSession`. See [#2480](https://github.com/DataDog/dd-sdk-android/pull/2480) + +# 2.17.0 / 2025-01-02 + +* [FEATURE] Implement the basic logic for `time-to-network-settle` view metric. See [#2397](https://github.com/DataDog/dd-sdk-android/pull/2392) +* [FEATURE] Add a Jetpack Compose fine grained masking override API. See [#2416](https://github.com/DataDog/dd-sdk-android/pull/2416) +* [FEATURE] Implement the basic logic for `interaction-to-next-view-metric`. See [#2417](https://github.com/DataDog/dd-sdk-android/pull/2417) +* [FEATURE] Support `is_main_process` property in telemetry configuration. See [#2422](https://github.com/DataDog/dd-sdk-android/pull/2422) +* [FEATURE] Introduce the `setNetworkSettledInitialResourceIdentifier` API. See [#2424](https://github.com/DataDog/dd-sdk-android/pull/2424) +* [FEATURE] Introduce the `setLastInteractionIdentifier` public API. See [#2428](https://github.com/DataDog/dd-sdk-android/pull/2428) +* [IMPROVEMENT] Refactor `TextSemanticsNodeMapper` to commonize the text wireframe logic. See [#2401](https://github.com/DataDog/dd-sdk-android/pull/2401) +* [IMPROVEMENT] Add `TextField` semantics mapper for Session Replay Compose. See [#2406](https://github.com/DataDog/dd-sdk-android/pull/2406) +* [IMPROVEMENT] Decouple Jetpack Compose reflection functions and report to Telemetry. See [#2415](https://github.com/DataDog/dd-sdk-android/pull/2415) +* [IMPROVEMENT] Move `ImageSemanticsMapper` reflection functions into `ReflectionUtils`. See [#2419](https://github.com/DataDog/dd-sdk-android/pull/2419) +* [IMPROVEMENT] Handle readOnly `additionalProperties`. See [#2423](https://github.com/DataDog/dd-sdk-android/pull/2423) +* [IMPROVEMENT] Improve the log message when exception happen during upload. See [#2411](https://github.com/DataDog/dd-sdk-android/pull/2411) +* [IMPROVEMENT] Apply global privacy level to semantics node mappers. See [#2413](https://github.com/DataDog/dd-sdk-android/pull/2413) +* [IMPROVEMENT] Apply touch privacy override in `RootSemanticsMapper`. See [#2421](https://github.com/DataDog/dd-sdk-android/pull/2421) +* [IMPROVEMENT] Get rid of `Thread.sleep(SHORT_SLEEP_MS)` in unit tests at `DatadogEventListenerTest`. See [#2430](https://github.com/DataDog/dd-sdk-android/pull/2430) +* [IMPROVEMENT] Add backwards compatibility for Coil `AsyncImage`. See [#2432](https://github.com/DataDog/dd-sdk-android/pull/2432) +* [IMPROVEMENT] Change the `logApiUsage` method signature - making event parameter computation lazy. See [#2433](https://github.com/DataDog/dd-sdk-android/pull/2433) +* [IMPROVEMENT] Apply the `hide` view override on Semantics nodes. See [#2434](https://github.com/DataDog/dd-sdk-android/pull/2434) +* [IMPROVEMENT] Correctly handle `TTNS` when a resource was stopped with an error. See [#2444](https://github.com/DataDog/dd-sdk-android/pull/2444) +* [IMPROVEMENT] Add integration tests for the `TTNS` metric. See [#2442](https://github.com/DataDog/dd-sdk-android/pull/2442) +* [IMPROVEMENT] Add integration tests for `ITNV` metric. See [#2445](https://github.com/DataDog/dd-sdk-android/pull/2445) +* [IMPROVEMENT] Display captured text when the text has `Ellipsis` overflow. See [#2446](https://github.com/DataDog/dd-sdk-android/pull/2446) +* [IMPROVEMENT] Support interop view from Jetpack Compose. See [#2452](https://github.com/DataDog/dd-sdk-android/pull/2452) +* [IMPROVEMENT] Add an "effective sample rate" to telemetry events. See [#2453](https://github.com/DataDog/dd-sdk-android/pull/2453) +* [IMPROVEMENT] Handle traces with coroutines. See [#2457](https://github.com/DataDog/dd-sdk-android/pull/2457) +* [IMPROVEMENT] Add support for Jetpack Compose Checkbox. See [#2414](https://github.com/DataDog/dd-sdk-android/pull/2414) +* [IMPROVEMENT] Replace `joinToString` when possible. See [#2456](https://github.com/DataDog/dd-sdk-android/pull/2456) +* [MAINTENANCE] Fix flaky test in the Deterministic Sampler. See [#2412](https://github.com/DataDog/dd-sdk-android/pull/2412) +* [MAINTENANCE] Fix `TextFieldSemanticsNodeMapper` flaky test. See [#2410](https://github.com/DataDog/dd-sdk-android/pull/2410) +* [MAINTENANCE] Fix potential issues with subdomain host lookups. See [#2436](https://github.com/DataDog/dd-sdk-android/pull/2436) +* [MAINTENANCE] Fix the way we are recording the last interaction for the `ITNV` metric. See [#2431](https://github.com/DataDog/dd-sdk-android/pull/2431) +* [MAINTENANCE] Deprecate Datadog `GlobalTracer` class. See [#2438](https://github.com/DataDog/dd-sdk-android/pull/2438) +* [MAINTENANCE] Correct the way we register the initial resources for the `TTNS` metric. See [#2439](https://github.com/DataDog/dd-sdk-android/pull/2439) +* [MAINTENANCE] Explicit Fragment dependency. See [#2443](https://github.com/DataDog/dd-sdk-android/pull/2443) +* [MAINTENANCE] Don't warn about missing views on `PerformanceMetric` events. See [#2454](https://github.com/DataDog/dd-sdk-android/pull/2454) + +# 2.16.1 / 2024-12-18 + +* [IMPROVEMENT] Refactoring for React Native Session Replay support. See [#2448](https://github.com/DataDog/dd-sdk-android/pull/2448) + +# 2.16.0 / 2024-11-20 + +* [FEATURE] Session Replay: Create Session Replay Compose module. + See [#1879](https://github.com/DataDog/dd-sdk-android/pull/1879) +* [FEATURE] Session Replay: Add `Tab` and `TabRow` Composable groups mappers. + See [#2171](https://github.com/DataDog/dd-sdk-android/pull/2171) +* [FEATURE] Session Replay: Add Abstract and Text semantics mapper for Compose Session Replay. + See [#2292](https://github.com/DataDog/dd-sdk-android/pull/2292) +* [FEATURE] Session Replay: Add Semantics Mapper for Button role. + See [#2296](https://github.com/DataDog/dd-sdk-android/pull/2296) +* [FEATURE] Session Replay: Add `ImageSemanticsNodeMapper` to support image role for Session Replay. + See [#2322](https://github.com/DataDog/dd-sdk-android/pull/2322) +* [FEATURE] Session Replay: Add Tab semantics mapper. + See [#2378](https://github.com/DataDog/dd-sdk-android/pull/2378) +* [FEATURE] Session Replay: Add `RadioButton` Semantics Node Mapper. + See [#2381](https://github.com/DataDog/dd-sdk-android/pull/2381) +* [FEATURE] Session Replay: Add Material Chip mapper and improve `CompoundButton` telemetry. + See [#2364](https://github.com/DataDog/dd-sdk-android/pull/2364) +* [FEATURE] Session Replay: Add Compose Session Replay scenario for benchmark sample application. + See [#2379](https://github.com/DataDog/dd-sdk-android/pull/2379) +* [FEATURE] Session Replay: Add multiple extension support. + See [#2384](https://github.com/DataDog/dd-sdk-android/pull/2384) +* [FEATURE] Session Replay: Add Compose Session Replay selector sample screen. + See [#2394](https://github.com/DataDog/dd-sdk-android/pull/2394) +* [FEATURE] Session Replay: Add `AndroidComposeViewMapper` to support popup. + See [#2395](https://github.com/DataDog/dd-sdk-android/pull/2395) +* [FEATURE] Session Replay: Integrate benchmark profiler in Compose mapper. + See [#2397](https://github.com/DataDog/dd-sdk-android/pull/2397) +* [IMPROVEMENT] Add `MethodCall` telemetry for compose mapper. + See [#2123](https://github.com/DataDog/dd-sdk-android/pull/2123) +* [IMPROVEMENT] Apply privacy settings to `TextCompositionGroupMapper` for Compose. + See [#2121](https://github.com/DataDog/dd-sdk-android/pull/2121) +* [IMPROVEMENT] Use `SurfaceCompositionGroupMapper` to support container components in Session + Replay. + See [#2182](https://github.com/DataDog/dd-sdk-android/pull/2182) +* [IMPROVEMENT] Fix padding and resizing issue for `ImageView` mapper. + See [#2372](https://github.com/DataDog/dd-sdk-android/pull/2372) +* [IMPROVEMENT] Add warning log when initializing the SDK outside of the main process. + See [#2376](https://github.com/DataDog/dd-sdk-android/pull/2376) +* [IMPROVEMENT] breaking API change: Allow typed `Sampler`. + See [#2385](https://github.com/DataDog/dd-sdk-android/pull/2385) +* [IMPROVEMENT] Create the `DeterministicSampler`. + See [#2387](https://github.com/DataDog/dd-sdk-android/pull/2387) +* [IMPROVEMENT] Use deterministic sampling by default when tracing. + See [#2388](https://github.com/DataDog/dd-sdk-android/pull/2388) +* [IMPROVEMENT] Align log levels for Session Replay already enabled. + See [#2399](https://github.com/DataDog/dd-sdk-android/pull/2399) +* [IMPROVEMENT] Adjust Webview Replay storage configuration limits. + See [#2400](https://github.com/DataDog/dd-sdk-android/pull/2400) +* [MAINTENANCE] Update Gradle to version `8.10.2`. + See [#2359](https://github.com/DataDog/dd-sdk-android/pull/2359) +* [MAINTENANCE] Fix `ButtonCompositionGroupMapper` crash while calculating the corner radius. + See [#2173](https://github.com/DataDog/dd-sdk-android/pull/2173) +* [MAINTENANCE] Fix Image reflection issue and update ProGuard rules. + See [#2337](https://github.com/DataDog/dd-sdk-android/pull/2337) +* [MAINTENANCE] Fix `CompoundButton` mapper drawable clone issue. + See [#2365](https://github.com/DataDog/dd-sdk-android/pull/2365) +* [MAINTENANCE] Fix crash while using recycled bitmap in Session Replay. + See [#2396](https://github.com/DataDog/dd-sdk-android/pull/2396) +* [MAINTENANCE] Add experimental annotation for Session Replay for Compose. + See [#2377](https://github.com/DataDog/dd-sdk-android/pull/2377) +* [MAINTENANCE] Lazy RUM raw event creation in event generator methods. + See [#2363](https://github.com/DataDog/dd-sdk-android/pull/2363) +* [MAINTENANCE] Remove legacy code using Compose `sourceInfo`. + See [#2386](https://github.com/DataDog/dd-sdk-android/pull/2386) + +# 2.15.1 / 2024-11-04 + +* [MAINTENANCE] Fix `resolveResourceId` not correctly calling job finished when drawable cloning + failed [#2367](https://github.com/DataDog/dd-sdk-android/pull/2367) + +# 2.15.0 / 2024-10-28 + +* [FEATURE] Add `TimeBank` in Session Replay recorder for dynamic optimisation See [#2247](https://github.com/DataDog/dd-sdk-android/pull/2247) +* [FEATURE] Add Session Replay skipped frames count in `session ended` metrics. See [#2256](https://github.com/DataDog/dd-sdk-android/pull/2256) +* [FEATURE] Add a touch privacy override. See [#2334](https://github.com/DataDog/dd-sdk-android/pull/2334) +* [FEATURE] Add precheck conditions when registering the Session Replay feature. See [#2264](https://github.com/DataDog/dd-sdk-android/pull/2264) +* [FEATURE] Add a privacy override for hidden views. See [#2291](https://github.com/DataDog/dd-sdk-android/pull/2291) +* [FEATURE] Add image and textAndInput privacy overrides. See [#2312](https://github.com/DataDog/dd-sdk-android/pull/2312) +* [IMPROVEMENT] Add a dynamic optimization configuration field in `SessionReplayConfiguration`. See [#2259](https://github.com/DataDog/dd-sdk-android/pull/2259) +* [IMPROVEMENT] Use layout text to display `TextView` overflow correctly. See [#2279](https://github.com/DataDog/dd-sdk-android/pull/2279) +* [IMPROVEMENT] Remove the Session Replay `ButtonMapper` border. See [#2280](https://github.com/DataDog/dd-sdk-android/pull/2280) +* [IMPROVEMENT] Force single core for Session Replay. See [#2324](https://github.com/DataDog/dd-sdk-android/pull/2324) +* [IMPROVEMENT] Add a `ViewGroups` Session Replay demo screen in sample app. See [#2285](https://github.com/DataDog/dd-sdk-android/pull/2285) +* [IMPROVEMENT] Run integration tests on API 35 in the testing pyramid. See [#2272](https://github.com/DataDog/dd-sdk-android/pull/2272) +* [IMPROVEMENT] Add `MaterialCardView` support in the Material Session Replay extension. See [#2290](https://github.com/DataDog/dd-sdk-android/pull/2290) +* [IMPROVEMENT] Use an SDK source value in the Session Replay `MobileSegment.source` property. See [#2293](https://github.com/DataDog/dd-sdk-android/pull/2293) +* [IMPROVEMENT] Update the Session Replay schema with a Kotlin Multiplatform source for Mobile segment. See [#2297](https://github.com/DataDog/dd-sdk-android/pull/2297) +* [IMPROVEMENT] Improve test coverage of core unit tests. See [#2294](https://github.com/DataDog/dd-sdk-android/pull/2294) +* [IMPROVEMENT] Improve unit test coverage for RUM, Logs and Trace features. See [#2299](https://github.com/DataDog/dd-sdk-android/pull/2299) +* [IMPROVEMENT] Send retry information into RUM data upload requests. See [#2298](https://github.com/DataDog/dd-sdk-android/pull/2298) +* [IMPROVEMENT] Make the `DataOkHttpUploader` state volatile. See [#2305](https://github.com/DataDog/dd-sdk-android/pull/2305) +* [IMPROVEMENT] Read Session Replay system requirements synchronously with strict mode allowance. See [#2307](https://github.com/DataDog/dd-sdk-android/pull/2307) +* [IMPROVEMENT] Override process importance for Session Replay integration tests. See [#2304](https://github.com/DataDog/dd-sdk-android/pull/2304) +* [IMPROVEMENT] Detekt the api coverage in integration tests. See [#2300](https://github.com/DataDog/dd-sdk-android/pull/2300) +* [IMPROVEMENT] Resolve `PorterDuffColorFilter` case in drawable to color mapper. See [#2319](https://github.com/DataDog/dd-sdk-android/pull/2319) +* [IMPROVEMENT] Prevent obfuscation of Fine Grained Masking enums. See [#2321](https://github.com/DataDog/dd-sdk-android/pull/2321) +* [IMPROVEMENT] Make sure `ConsentAwareFileOrchestrator` is thread safe. See [#2313](https://github.com/DataDog/dd-sdk-android/pull/2313) +* [IMPROVEMENT] Improve RUM integration tests. See [#2317](https://github.com/DataDog/dd-sdk-android/pull/2317) +* [IMPROVEMENT] Add a default sample rate for Session Replay. See [#2323](https://github.com/DataDog/dd-sdk-android/pull/2323) +* [IMPROVEMENT] Remove batch metrics inner sampler to increase sample rate. See [#2328](https://github.com/DataDog/dd-sdk-android/pull/2328) +* [IMPROVEMENT] Add missing integration test for Logs. See [#2330](https://github.com/DataDog/dd-sdk-android/pull/2330) +* [IMPROVEMENT] Update Session Replay integration test payloads. See [#2318](https://github.com/DataDog/dd-sdk-android/pull/2318) +* [MAINTENANCE] Update Datadog Agent to 1.41.0. See [#2331](https://github.com/DataDog/dd-sdk-android/pull/2331) +* [MAINTENANCE] Fix the decompression in Session Replay instrumented tests for API 21. See [#2341](https://github.com/DataDog/dd-sdk-android/pull/2341) +* [MAINTENANCE] Reactivate Session Replay instrumented test for API 21. See [#2342](https://github.com/DataDog/dd-sdk-android/pull/2342) +* [MAINTENANCE] Fix some flaky tests. See [#2281](https://github.com/DataDog/dd-sdk-android/pull/2281) +* [MAINTENANCE] Fix a StrictMode warning regarding I/O disk operation on the main thread. See [#2284](https://github.com/DataDog/dd-sdk-android/pull/2284) +* [MAINTENANCE] Fix flaky feature context integration tests. See [#2295](https://github.com/DataDog/dd-sdk-android/pull/2295) +* [MAINTENANCE] Fix `SeekBarWireframeMapper` flaky test. See [#2308](https://github.com/DataDog/dd-sdk-android/pull/2308) +* [MAINTENANCE] Fix `SpanEventSerializerTest` flakiness. See [#2311](https://github.com/DataDog/dd-sdk-android/pull/2311) +* [MAINTENANCE] Remove an unnecessary legacy privacy line from the sampleApplication. See [#2314](https://github.com/DataDog/dd-sdk-android/pull/2314) +* [MAINTENANCE] Use Java 11 bytecode for public modules. See [#2315](https://github.com/DataDog/dd-sdk-android/pull/2315) +* [MAINTENANCE] Fix RUM integration test `verifyViewEventsOnSwipe`. See [#2326](https://github.com/DataDog/dd-sdk-android/pull/2326) +* [MAINTENANCE] Fix the regression for the `TelemetryErrorEvent` with throwable. See [#2325](https://github.com/DataDog/dd-sdk-android/pull/2325) +* [MAINTENANCE] Fix the execution of legacy instrumentation tests in CI. See [#2329](https://github.com/DataDog/dd-sdk-android/pull/2329) + +# 2.14.0 / 2024-09-25 + +* [FEATURE] Add stop and start APIs for Session Replay. See [#2169](https://github.com/DataDog/dd-sdk-android/pull/2169) +* [FEATURE] Add touch privacy fine grained masking API to Session Replay. See [#2196](https://github.com/DataDog/dd-sdk-android/pull/2196) +* [FEATURE] Add text and input privacy fine grained masking API to Session Replay. See [#2235](https://github.com/DataDog/dd-sdk-android/pull/2235) +* [FEATURE] Introduce the `RumMonitor#addViewLoadingTime` API. See [#2243](https://github.com/DataDog/dd-sdk-android/pull/2243) +* [FEATURE] Introduce the API usage telemetry event and API. See [#2258](https://github.com/DataDog/dd-sdk-android/pull/2258) +* [IMPROVEMENT] Enable Kotlin test fixtures support. See [#2234](https://github.com/DataDog/dd-sdk-android/pull/2234) +* [IMPROVEMENT] Add `isContainer` attribute to session replay span. See [#2244](https://github.com/DataDog/dd-sdk-android/pull/2244) +* [IMPROVEMENT] Update custom detekt CI Job. See [#2118](https://github.com/DataDog/dd-sdk-android/pull/2118) +* [IMPROVEMENT] Randomize privacy levels to support Fine Grained Masking in E2E. See [#2265](https://github.com/DataDog/dd-sdk-android/pull/2265) +* [IMPROVEMENT] Update AGP to 8.6.1. See [#2269](https://github.com/DataDog/dd-sdk-android/pull/2269) +* [IMPROVEMENT] Add telemetry and logs related with `RumMonitor#addViewLoadingTime` API. See [#2267](https://github.com/DataDog/dd-sdk-android/pull/2267) +* [IMPROVEMENT] Handle SSE requests. See [#2270](https://github.com/DataDog/dd-sdk-android/pull/2270) +* [IMPROVEMENT] Do not use magic numbers in `InternalLogger` API. See [#2271](https://github.com/DataDog/dd-sdk-android/pull/2271) +* [IMPROVEMENT] Optimize MD5 byte array to hex string conversion. See [#2273](https://github.com/DataDog/dd-sdk-android/pull/2273) +* [IMPROVEMENT] `CONTRIBUTING` doc changes. See [#2275](https://github.com/DataDog/dd-sdk-android/pull/2275) +* [IMPROVEMENT] Add env tag in benchmark metrics. See [#2276](https://github.com/DataDog/dd-sdk-android/pull/2276) +* [MAINTENANCE] Make image privacy fine grained masking API public in Session Replay. See [#2204](https://github.com/DataDog/dd-sdk-android/pull/2204) +* [MAINTENANCE] Update benchmark metrics memory reader probe interval. See [#2228](https://github.com/DataDog/dd-sdk-android/pull/2228) +* [MAINTENANCE] Fix the flakiness in the `KioskTrackingTest`. See [#2226](https://github.com/DataDog/dd-sdk-android/pull/2226) +* [MAINTENANCE] Fix placeholder dimensions. See [#2248](https://github.com/DataDog/dd-sdk-android/pull/2248) +* [MAINTENANCE] Send fine grained masking instead of legacy privacy in config telemetry. See [#2253](https://github.com/DataDog/dd-sdk-android/pull/2253) +* [MAINTENANCE] Ensure `UploadWorker` uses the SDK instance name. See [#2257](https://github.com/DataDog/dd-sdk-android/pull/2257) +* [MAINTENANCE] Explicitly set `antlr-runtime` transitive dependency version. See [#2261](https://github.com/DataDog/dd-sdk-android/pull/2261) +* [MAINTENANCE] Add the integration tests related with `RumMonitor#addViewLoadingTime` API. See [#2268](https://github.com/DataDog/dd-sdk-android/pull/2268) +* [MAINTENANCE] Fix `DatadogInterceptor` flaky test. See [#2274](https://github.com/DataDog/dd-sdk-android/pull/2274) +* [MAINTENANCE] Fix typos and links in Github issue templates. See [#2277](https://github.com/DataDog/dd-sdk-android/pull/2277) + +# 2.13.1 / 2024-09-09 + +* [BUGFIX] Stop upload worker on upload failure. See [#2242](https://github.com/DataDog/dd-sdk-android/pull/2242) + +# 2.13.0 / 2024-09-03 + +* [FEATURE] Create Benchmark module to collect performance metrics. See [#2141](https://github.com/DataDog/dd-sdk-android/pull/2141) +* [BUGFIX] Use NO_EXPORT_FLAG for BroadcastReceiver on API above 26. See [#2170](https://github.com/DataDog/dd-sdk-android/pull/2170) +* [BUGFIX] Fix integration tests pipeline for API 21. See [#2197](https://github.com/DataDog/dd-sdk-android/pull/2197) +* [IMPROVEMENT] Added setSyntheticsAttribute in RumInternalProxy. See [#2133](https://github.com/DataDog/dd-sdk-android/pull/2133) +* [IMPROVEMENT] Use macos runner. See [#2154](https://github.com/DataDog/dd-sdk-android/pull/2154) +* [IMPROVEMENT] Remove obsolete nightly test references. See [#2157](https://github.com/DataDog/dd-sdk-android/pull/2157) +* [IMPROVEMENT] Add the integration tests for the SdkCore APIs. See [#2145](https://github.com/DataDog/dd-sdk-android/pull/2145) +* [IMPROVEMENT] Update link to troubleshooting documentation. See [#2164](https://github.com/DataDog/dd-sdk-android/pull/2164) (Thanks [@mateo-villa](https://github.com/mateo-villa)) +* [IMPROVEMENT] Reset developerMode status when Datadog stop. See [#2174](https://github.com/DataDog/dd-sdk-android/pull/2174) +* [IMPROVEMENT] Extract logic to pull publishing credentials into a dedicated snippet. See [#2176](https://github.com/DataDog/dd-sdk-android/pull/2176) +* [IMPROVEMENT] Remove redundant build configuration in new reliability modules. See [#2178](https://github.com/DataDog/dd-sdk-android/pull/2178) +* [IMPROVEMENT] Remove image property from macOS-based jobs. See [#2181](https://github.com/DataDog/dd-sdk-android/pull/2181) +* [IMPROVEMENT] Update OkHttp to 4.12.0. See [#1975](https://github.com/DataDog/dd-sdk-android/pull/1975) +* [IMPROVEMENT] Speed up `IdGenerationStrategy` test. See [#2187](https://github.com/DataDog/dd-sdk-android/pull/2187) +* [IMPROVEMENT] Add integration tests for internal sdk core. See [#2177](https://github.com/DataDog/dd-sdk-android/pull/2177) +* [IMPROVEMENT] Update Gradle to 8.9 and AGP to 8.5.2. See [#2192](https://github.com/DataDog/dd-sdk-android/pull/2192) +* [IMPROVEMENT] Speed up generated files/licenses checks. See [#2188](https://github.com/DataDog/dd-sdk-android/pull/2188) +* [IMPROVEMENT] Log Timber tag. See [#2202](https://github.com/DataDog/dd-sdk-android/pull/2202) +* [IMPROVEMENT] Make sure user properties are immutable when setUserInfo. See [#2203](https://github.com/DataDog/dd-sdk-android/pull/2203) +* [IMPROVEMENT] Add the integration tests for FeatureScope public API. See [#2209](https://github.com/DataDog/dd-sdk-android/pull/2209) +* [IMPROVEMENT] Include optional exception in Upload Status. See [#2221](https://github.com/DataDog/dd-sdk-android/pull/2221) +* [IMPROVEMENT] Create UploadSchedulerStrategy interface and default implementation. See [#2222](https://github.com/DataDog/dd-sdk-android/pull/2222) +* [IMPROVEMENT] Add configuration to set uploadSchedulerStrategy. See [#2224](https://github.com/DataDog/dd-sdk-android/pull/2224) +* [IMPROVEMENT] Update `kotlinx.ast` dependency. See [#2231](https://github.com/DataDog/dd-sdk-android/pull/2231) + +# 2.12.1 / 2024-08-13 + +* [BUGFIX] RUM: Make no-op RUM monitor implementation returned by default to be `NoOpAdvancedRumMonitor`. See [#2185](https://github.com/DataDog/dd-sdk-android/pull/2185) + +# 2.12.0 / 2024-07-30 + +* [FEATURE] Trace: Add the `SessionEndedMetric` into sdk core. See [#2090](https://github.com/DataDog/dd-sdk-android/pull/2090) +* [FEATURE] SessionReplay: Use the datastore for Session Replay resources. See [#2041](https://github.com/DataDog/dd-sdk-android/pull/2041) +* [FEATURE] Trace: Provide 128 bits support for the trace ids in the Tracing sdk. See [#2089](https://github.com/DataDog/dd-sdk-android/pull/2089) +* [FEATURE] SessionReplay: Add api to clear all datastore data. See [#2096](https://github.com/DataDog/dd-sdk-android/pull/2096) +* [FEATURE] SessionReplay: Add `CompoundButton` mapper. See [#2120](https://github.com/DataDog/dd-sdk-android/pull/2120) +* [FEATURE] SessionReplay: Add API to configure the Image Privacy. See [#2125](https://github.com/DataDog/dd-sdk-android/pull/2125) +* [FEATURE] Trace: Introduce the `TraceContextInjection` to handle sampling in distributed traces. See [#2111](https://github.com/DataDog/dd-sdk-android/pull/2111) +* [IMPROVEMENT] Trace: Improve unit tests in Session metrics. See [#2095](https://github.com/DataDog/dd-sdk-android/pull/2095) +* [IMPROVEMENT] SessionReplay: Fix flaky test in `SeekBarWireframeMapperTest`. See [#2099](https://github.com/DataDog/dd-sdk-android/pull/2099) +* [IMPROVEMENT] Trace: Fix the Okhttp Otel parent span feature when not using RUM. See [#2100](https://github.com/DataDog/dd-sdk-android/pull/2100) +* [IMPROVEMENT] SessionReplay: Fix units for dropped nodes. See [#2107](https://github.com/DataDog/dd-sdk-android/pull/2107) +* [IMPROVEMENT] SessionReplay: Add TLVFormat DataStore persistence. See [#2038](https://github.com/DataDog/dd-sdk-android/pull/2038) +* [IMPROVEMENT] InternalMetrics: Add sampling rate to internal metrics. See [#2108](https://github.com/DataDog/dd-sdk-android/pull/2108) +* [IMPROVEMENT] SessionReplay: Fix `RumSessionEnded` metric flaky test. See [#2114](https://github.com/DataDog/dd-sdk-android/pull/2114) +* [IMPROVEMENT] SessionReplay: Use `BackpressureExecutor` for SessionReplay event processing. See [#2116](https://github.com/DataDog/dd-sdk-android/pull/2116) +* [IMPROVEMENT] SessionReplay: Improve CheckableTextViewMapper. See [#2115](https://github.com/DataDog/dd-sdk-android/pull/2115) +* [IMPROVEMENT] SessionReplay: `SwitchCompat` mapper improvement. See [#2117](https://github.com/DataDog/dd-sdk-android/pull/2117) +* [IMPROVEMENT] RUM: Fix the racing condition in the `RotatingDnsResolver` logic. See [#2127](https://github.com/DataDog/dd-sdk-android/pull/2127) +* [IMPROVEMENT] RUM: Add request id in okhttp request. See [#2126](https://github.com/DataDog/dd-sdk-android/pull/2126) +* [IMPROVEMENT] Trace: Make sure network local spans have `kind:client` tag. See [#2136](https://github.com/DataDog/dd-sdk-android/pull/2136) +* [IMPROVEMENT] Core: Increase retry delay on DNS error. See [#2135](https://github.com/DataDog/dd-sdk-android/pull/2135) + +# 2.11.0 / 2024-06-20 + +* [FEATURE] Trace: Bundle `dd-trace-core` code into the `dd-sdk-android-trace` module. See [#1907](https://github.com/DataDog/dd-sdk-android/pull/1907) +* [FEATURE] Trace: Provide the correct sampling priority for our Span events based on APM new rules. See [#1913](https://github.com/DataDog/dd-sdk-android/pull/1913) +* [FEATURE] Trace: Add the `CoreTracer` tests. See [#1924](https://github.com/DataDog/dd-sdk-android/pull/1924) +* [FEATURE] Trace: Provide core tracer logger implementation. See [#1953](https://github.com/DataDog/dd-sdk-android/pull/1953) +* [FEATURE] Trace: Provide the `bundleWithRum` capability for `OtelTracer`. See [#1960](https://github.com/DataDog/dd-sdk-android/pull/1960) +* [FEATURE] Trace: Provide the `DatadogContextStorage` for OpenTelemetry. See [#1970](https://github.com/DataDog/dd-sdk-android/pull/1970) +* [FEATURE] Trace: Provide Otel bundle with logs feature. See [#1979](https://github.com/DataDog/dd-sdk-android/pull/1979) +* [FEATURE] Trace: Setup the trace end tests environment for Otel API. See [#1983](https://github.com/DataDog/dd-sdk-android/pull/1983) +* [FEATURE] Trace: Add the `SpanLink` support for Otel API implementation. See [#1993](https://github.com/DataDog/dd-sdk-android/pull/1993) +* [FEATURE] Trace: Add the Otel API feature integration tests. See [#1995](https://github.com/DataDog/dd-sdk-android/pull/1995) +* [FEATURE] Trace: Report OpenTelemetry data in the configuration telemetry. See [#2006](https://github.com/DataDog/dd-sdk-android/pull/2006) +* [FEATURE] Trace: Extract OpenTelemetry support SDK into a dedicated module. See [#2021](https://github.com/DataDog/dd-sdk-android/pull/2021) +* [FEATURE] Trace: Setup the CI and Gradle tests for the new `dd-sdk-android-trace-otel` module. See [#2035](https://github.com/DataDog/dd-sdk-android/pull/2035) +* [FEATURE] Trace: Enable desugaring for sample and single-fit apps. See [#2036](https://github.com/DataDog/dd-sdk-android/pull/2036) +* [FEATURE] Session Replay: Add support for progress bars. See [#2047](https://github.com/DataDog/dd-sdk-android/pull/2047) +* [FEATURE] Trace: Add OpenTelemetry use case into the Wear sample app. See [#2068](https://github.com/DataDog/dd-sdk-android/pull/2068) +* [FEATURE] Trace: Add OpenTelemetry use case into the `vendor-lib` sample. See [#2069](https://github.com/DataDog/dd-sdk-android/pull/2069) +* [FEATURE] Trace: Add the OkHttp Otel extensions module. See [#2073](https://github.com/DataDog/dd-sdk-android/pull/2073) +* [FEATURE] Trace: `OtelTraceProvider.Builder`: introduce the trace rate limit property. See [#2086](https://github.com/DataDog/dd-sdk-android/pull/2086) +* **WARNING**: Existing `com.datadog.trace` package renamed to `com.datadog.legacy.trace`. `com.datadog.trace` package will contain new members, so update your imports accordingly. +* [BUGFIX] Session Replay: Fix time drift in `RecordedDataQueueHandler`. See [#2075](https://github.com/DataDog/dd-sdk-android/pull/2075) +* [IMPROVEMENT] Trace: Remove some unused IAST/CI Visibility classes. See [#2000](https://github.com/DataDog/dd-sdk-android/pull/2000) +* [IMPROVEMENT] Trace: Remove `moshi` dependency from trace module. See [#2003](https://github.com/DataDog/dd-sdk-android/pull/2003) +* [IMPROVEMENT] Fix some detekt issues. See [#2043](https://github.com/DataDog/dd-sdk-android/pull/2043) +* [IMPROVEMENT] Session Replay: Delegate `Drawable` copy to background thread. See [#2048](https://github.com/DataDog/dd-sdk-android/pull/2048) +* [IMPROVEMENT] Trace: Make `CoreTracer` code Java 7 compatible. See [#2051](https://github.com/DataDog/dd-sdk-android/pull/2051) +* [IMPROVEMENT] Session Replay: Improve telemetry from `RecordedDataQueueHandler`. See [#2053](https://github.com/DataDog/dd-sdk-android/pull/2053) +* [IMPROVEMENT] Global: Fix thread safety warnings. See [#2056](https://github.com/DataDog/dd-sdk-android/pull/2056) +* [IMPROVEMENT] Trace: Remove the `dd-sketches` dependency and related logic. See [#2062](https://github.com/DataDog/dd-sdk-android/pull/2062) +* [IMPROVEMENT] Trace: Fix the `jctools` Proguard rules. See [#2063](https://github.com/DataDog/dd-sdk-android/pull/2063) +* [IMPROVEMENT] Add ProGuard rules to sample app. See [#2067](https://github.com/DataDog/dd-sdk-android/pull/2067) +* [IMPROVEMENT] Session Replay: Improve `ButtonMapper`. See [#2070](https://github.com/DataDog/dd-sdk-android/pull/2070) +* [IMPROVEMENT] Trace: Remove some unused code from tracing module. See [#2079](https://github.com/DataDog/dd-sdk-android/pull/2079) +* [IMPROVEMENT] Trace: Add OpenTelemetry Proguard rules for compile-only annotations. See [#2080](https://github.com/DataDog/dd-sdk-android/pull/2080) +* [IMPROVEMENT] Trace: Fix the `CoreTracer` flaky tests. See [#2081](https://github.com/DataDog/dd-sdk-android/pull/2081) +* [IMPROVEMENT] Trace: Remove System and Environment config source in the `CoreTracer`. See [#2084](https://github.com/DataDog/dd-sdk-android/pull/2084) +* [IMPROVEMENT] Remove duplicated Proguard configuration in the sample app. See [#2088](https://github.com/DataDog/dd-sdk-android/pull/2088) +* [IMPROVEMENT] Session Replay: Granular telemetry sampling for mappers. See [#2087](https://github.com/DataDog/dd-sdk-android/pull/2087) +* [MAINTENANCE] Merge develop branch. See [#1948](https://github.com/DataDog/dd-sdk-android/pull/1948) +* [MAINTENANCE] Merge `develop` branch into `feature/otel-support` branch. See [#1998](https://github.com/DataDog/dd-sdk-android/pull/1998) +* [MAINTENANCE] Next dev iteration 2.11.0. See [#2050](https://github.com/DataDog/dd-sdk-android/pull/2050) +* [MAINTENANCE] Merge `release/2.10.0` branch into `develop` branch. See [#2054](https://github.com/DataDog/dd-sdk-android/pull/2054) +* [MAINTENANCE] Merge `develop` branch into `feature/otel-support` branch. See [#2058](https://github.com/DataDog/dd-sdk-android/pull/2058) +* [MAINTENANCE] Merge release `2.10.1` into `develop` branch. See [#2065](https://github.com/DataDog/dd-sdk-android/pull/2065) +* [MAINTENANCE] Merge develop branch. See [#2076](https://github.com/DataDog/dd-sdk-android/pull/2076) +* [MAINTENANCE] Merge Otel feature branch. See [#2077](https://github.com/DataDog/dd-sdk-android/pull/2077) + +# 2.10.1 / 2024-05-30 + +* [IMPROVEMENT] Reduce Method Call Sample Rate. See [#2060](https://github.com/DataDog/dd-sdk-android/pull/2060) +* [IMPROVEMENT] Limit total telemetry events sent per session. See [#2061](https://github.com/DataDog/dd-sdk-android/pull/2061) + +# 2.10.0 / 2024-05-23 + +* [FEATURE] Global: Add Method Call Telemetry. See [#1940](https://github.com/DataDog/dd-sdk-android/pull/1940) +* [FEATURE] Session Replay: Add support to the `Toolbar` in Session Replay. See [#2024](https://github.com/DataDog/dd-sdk-android/pull/2024) +* [IMPROVEMENT] Session Replay: Improve masking arch. See [#2011](https://github.com/DataDog/dd-sdk-android/pull/2011) +* [IMPROVEMENT] Session Replay: Simplify generic type in mappers. See [#2015](https://github.com/DataDog/dd-sdk-android/pull/2015) +* [IMPROVEMENT] Global: Support additional properties in Telemetry Error events. See [#2025](https://github.com/DataDog/dd-sdk-android/pull/2025) +* [IMPROVEMENT] Session Replay: Add telemetry on SR resources track. See [#2027](https://github.com/DataDog/dd-sdk-android/pull/2027) +* [IMPROVEMENT] Session Replay: Add telemetry to detect uncovered View/Drawable in Session Replay. See [#2028](https://github.com/DataDog/dd-sdk-android/pull/2028) +* [IMPROVEMENT] Session Replay: Improve `SeekBarMapper`. See [#2037](https://github.com/DataDog/dd-sdk-android/pull/2037) +* [IMPROVEMENT] RUM: Flag critical events in custom persistence. See [#2044](https://github.com/DataDog/dd-sdk-android/pull/2044) +* [IMPROVEMENT] Delegate Drawable copy to background thread. See [#2048](https://github.com/DataDog/dd-sdk-android/pull/2048) +* [MAINTENANCE] Next dev iteration. See [#2020](https://github.com/DataDog/dd-sdk-android/pull/2020) +* [MAINTENANCE] Merge release `2.9.0` into `develop` branch. See [#2023](https://github.com/DataDog/dd-sdk-android/pull/2023) +* [MAINTENANCE] Session Replay: Improve UT for SR Obfuscators. See [#2031](https://github.com/DataDog/dd-sdk-android/pull/2031) +* [MAINTENANCE] Create package name consistency rule. See [#2032](https://github.com/DataDog/dd-sdk-android/pull/2032) +* [MAINTENANCE] Session Replay: Improve the `TextViewMapper` unit tests. See [#2034](https://github.com/DataDog/dd-sdk-android/pull/2034) +* [MAINTENANCE] Fix KtLint version in `local_ci` script. See [#2039](https://github.com/DataDog/dd-sdk-android/pull/2039) +* [MAINTENANCE] Session Replay: Fix SR flaky test. See [#2042](https://github.com/DataDog/dd-sdk-android/pull/2042) +* [MAINTENANCE] Global: Update the Method Call metric usage. See [#2040](https://github.com/DataDog/dd-sdk-android/pull/2040) +* [MAINTENANCE] Update static analysis pipeline version. See [#2045](https://github.com/DataDog/dd-sdk-android/pull/2045) +* [MAINTENANCE] Fix flaky test regarding `PerformanceMeasure` sampling rate. See [#2046](https://github.com/DataDog/dd-sdk-android/pull/2046) + +# 2.9.0 / 2024-05-02 + +* [BUGFIX] RUM: Prevent crash in `JankStats` listener. See [#1981](https://github.com/DataDog/dd-sdk-android/pull/1981) +* [BUGFIX] RUM: Unregister vital listeners when view is stopped. See [#2009](https://github.com/DataDog/dd-sdk-android/pull/2009) +* [BUGFIX] Core: Fix `ConcurrentModificationException` during features iteration. See [#2012](https://github.com/DataDog/dd-sdk-android/pull/2012) +* [IMPROVEMENT] RUM: Optimise `BatchFileOrchestator` performance. See [#1968](https://github.com/DataDog/dd-sdk-android/pull/1968) +* [IMPROVEMENT] Use custom naming for threads created inside SDK. See [#1987](https://github.com/DataDog/dd-sdk-android/pull/1987) +* [IMPROVEMENT] Synchronize SR info with webviews. See [#1990](https://github.com/DataDog/dd-sdk-android/pull/1990) +* [IMPROVEMENT] Core: Start sending batches immediately after feature is initialized. See [#1991](https://github.com/DataDog/dd-sdk-android/pull/1991) +* [IMRPOVEMENT] Create RUM Feature Integration Tests. See [#2004](https://github.com/DataDog/dd-sdk-android/pull/2004) +* [IMRROVEMENT] Make constructors of `DatadogSite` private. See [#2010](https://github.com/DataDog/dd-sdk-android/pull/2010) +* [IMRROVEMENT] Log warning about tag modification only once. See [#2017](https://github.com/DataDog/dd-sdk-android/pull/2017) +* [IMRROVEMENT] Add status code in user-facing message in case of `UnknownError` during batch upload. See [#2018](https://github.com/DataDog/dd-sdk-android/pull/2018) +* [MAINTENANCE] Next dev iteration. See [#1972](https://github.com/DataDog/dd-sdk-android/pull/1972) +* [MAINTENANCE] Remove non-ASCII characters from test names. See [#1973](https://github.com/DataDog/dd-sdk-android/pull/1973) +* [MAINTENANCE] Update Kotlin to 1.8.22, Gradle to 8.2.1, update related tooling. See [#1974](https://github.com/DataDog/dd-sdk-android/pull/1974) +* [MAINTENANCE] Merge `release/2.8.0` branch into `develop` branch. See [#1977](https://github.com/DataDog/dd-sdk-android/pull/1977) +* [MAINTENANCE] Switch to the Golden Base Image for Docker. See [#1982](https://github.com/DataDog/dd-sdk-android/pull/1982) +* [MAINTENANCE] Remove unused Maven Model dependency. See [#1989](https://github.com/DataDog/dd-sdk-android/pull/1989) +* [MAINTENANCE] Update testing ci steps to limit OOM and memory usage. See [#1986](https://github.com/DataDog/dd-sdk-android/pull/1986) +* [MAINTENANCE] Upload sample app to rum playground. See [#1994](https://github.com/DataDog/dd-sdk-android/pull/1994) +* [MAINTENANCE] Update copyright. See [#1992](https://github.com/DataDog/dd-sdk-android/pull/1992) +* [MAINTENANCE] Don't mark internal extension functions for 3rd party types as 3rd party. See [#1996](https://github.com/DataDog/dd-sdk-android/pull/1996) +* [MAINTENANCE] Use credentials for the right org. See [#1997](https://github.com/DataDog/dd-sdk-android/pull/1997) +* [MAINTENANCE] Update Detekt API version used to 1.23.0. See [#1988](https://github.com/DataDog/dd-sdk-android/pull/1988) +* [MAINTENANCE] Remove the usage of deprecated `TestConfig` constructor. See [#1999](https://github.com/DataDog/dd-sdk-android/pull/1999) +* [MAINTENANCE] Fix flakyness in SR unit tests. See [#2001](https://github.com/DataDog/dd-sdk-android/pull/2001) +* [MAINTENANCE] Remove legacy nightly tests. See [#2005](https://github.com/DataDog/dd-sdk-android/pull/2005) +* [MAINTENANCE] Redirect slack notif to mobile-sdk-ops channel. See [#2007](https://github.com/DataDog/dd-sdk-android/pull/2007) + +# 2.8.0 / 2024-04-09 + +* [FEATURE] Add `buildId` to the RUM error and Log events. See [#1756](https://github.com/DataDog/dd-sdk-android/pull/1756) +* [FEATURE] WebView Session Replay: Implement WebView bridge getCapabilities. See [#1871](https://github.com/DataDog/dd-sdk-android/pull/1871) +* [FEATURE] RUM: Call RUM error mapper even for crashes. See [#1945](https://github.com/DataDog/dd-sdk-android/pull/1945) +* [FEATURE] RUM: Report time since the application start for crashes in RUM. See [#1961](https://github.com/DataDog/dd-sdk-android/pull/1961) +* [BUGFIX] RUM: Fix application startup time regression. See [#1935](https://github.com/DataDog/dd-sdk-android/pull/1935) +* [BUGFIX] Session Replay: Prevent crashing the host app in the `ViewOnDrawInterceptor`. See [#1951](https://github.com/DataDog/dd-sdk-android/pull/1951) +* [BUGFIX] Session Replay: Prevent crash in Canvas Wrapper. See [#1954](https://github.com/DataDog/dd-sdk-android/pull/1954) +* [BUGFIX] RUM: Safe getting of Intent extras. See [#1950](https://github.com/DataDog/dd-sdk-android/pull/1950) +* [BUGFIX] RUM: Don't traverse non-visible ViewGroups for searching user interaction targets. See [#1969](https://github.com/DataDog/dd-sdk-android/pull/1969) +* [IMPROVEMENT] WebView Session Replay: Introduce the `FeatureContextUpdateListener` API. See [#1829](https://github.com/DataDog/dd-sdk-android/pull/1829) +* [IMPROVEMENT] WebView Session Replay: Provide the parent container information for browser rum events. See [#1831](https://github.com/DataDog/dd-sdk-android/pull/1831) +* [IMPROVEMENT] WebView Session Replay: Detect full snapshot from WebView session replay. See [#1908](https://github.com/DataDog/dd-sdk-android/pull/1908) +* [IMPROVEMENT] Session Replay: Refactor and split classes. See [#1873](https://github.com/DataDog/dd-sdk-android/pull/1873) +* [IMPROVEMENT] WebView Session Replay: Keep WebView wireframe hidden. See [#1949](https://github.com/DataDog/dd-sdk-android/pull/1949) +* [IMPROVEMENT] Remove Runtime shutdown hook when SDK instance is stopped. See [#1956](https://github.com/DataDog/dd-sdk-android/pull/1956) +* [IMPROVEMENT] Fix message when writer is NoOp. See [#1963](https://github.com/DataDog/dd-sdk-android/pull/1963) +* [IMPROVEMENT] Global: Make sure `error.threads` always have content from `error.stack`. See [#1964](https://github.com/DataDog/dd-sdk-android/pull/1964) +* [MAINTENANCE] Merge develop branch. See [#1849](https://github.com/DataDog/dd-sdk-android/pull/1849) +* [MAINTENANCE] Merge develop. See [#1915](https://github.com/DataDog/dd-sdk-android/pull/1915) +* [MAINTENANCE] Merge develop into Session Replay WebView feature branch. See [#1917](https://github.com/DataDog/dd-sdk-android/pull/1917) +* [MAINTENANCE] Merge develop into `feature/sr-webview`. See [#1922](https://github.com/DataDog/dd-sdk-android/pull/1922) +* [MAINTENANCE] Next dev iteration. See [#1928](https://github.com/DataDog/dd-sdk-android/pull/1928) +* [MAINTENANCE] Merge release 2.7.0 into `develop` branch. See [#1930](https://github.com/DataDog/dd-sdk-android/pull/1930) +* [MAINTENANCE] Address some flaky tests. See [#1934](https://github.com/DataDog/dd-sdk-android/pull/1934) +* [MAINTENANCE] Add a test for the safe events serialization produced by `RumViewScope` in multi-threaded environment. See [#1933](https://github.com/DataDog/dd-sdk-android/pull/1933) +* [MAINTENANCE] Fix mime type for nightly tests. See [#1936](https://github.com/DataDog/dd-sdk-android/pull/1936) +* [MAINTENANCE] Disable some Session Replay integration tests temporarily due to flakiness. See [#1941](https://github.com/DataDog/dd-sdk-android/pull/1941) +* [MAINTENANCE] Merge 2.7.1 on develop. See [#1947](https://github.com/DataDog/dd-sdk-android/pull/1947) +* [MAINTENANCE] Improve TODO detekt rule. See [#1955](https://github.com/DataDog/dd-sdk-android/pull/1955) +* [MAINTENANCE] Disable flaky Session Replay test. See [#1957](https://github.com/DataDog/dd-sdk-android/pull/1957) +* [MAINTENANCE] Merge develop. See [#1958](https://github.com/DataDog/dd-sdk-android/pull/1958) +* [MAINTENANCE] Merge `feature/sr-web-view-support`. See [#1959](https://github.com/DataDog/dd-sdk-android/pull/1959) +* [MAINTENANCE] Fix flaky `TodoWithoutTask` tests. See [#1962](https://github.com/DataDog/dd-sdk-android/pull/1962) +* [MAINTENANCE] Fix flaky `DatadogCore` test. See [#1965](https://github.com/DataDog/dd-sdk-android/pull/1965) +* [MAINTENANCE] Update actions for running CodeQL workflow. See [#1966](https://github.com/DataDog/dd-sdk-android/pull/1966) +* [MAINTENANCE] Fix flaky tests. See [#1967](https://github.com/DataDog/dd-sdk-android/pull/1967) + +# 2.7.1 / 2024-03-27 + +* [BUGFIX] RUM: Improve adding Feature Flag evaluation(s) performance. + See [#1932](https://github.com/DataDog/dd-sdk-android/pull/1932) +* [MAINTENANCE] Core: add a BackPressure strategy to limit the load on background threads and get notified when capacity is reached. + See [#1938](https://github.com/DataDog/dd-sdk-android/pull/1938) and [#1939](https://github.com/DataDog/dd-sdk-android/pull/1939) + +# 2.7.0 / 2024-03-21 + +* [FEATURE] Session Replay: Add a request builder for resources. See [#1827](https://github.com/DataDog/dd-sdk-android/pull/1827) +* [FEATURE] Session Replay: Add Resources feature. See [#1840](https://github.com/DataDog/dd-sdk-android/pull/1840) +* [FEATURE] Session Replay: Implement resource capture during traversal. See [#1854](https://github.com/DataDog/dd-sdk-android/pull/1854) +* [FEATURE] Add `source_type` when sent from cross platform logs. See [#1895](https://github.com/DataDog/dd-sdk-android/pull/1895) +* [FEATURE] Session Replay: Enable Resource Endpoint by default. See [#1858](https://github.com/DataDog/dd-sdk-android/pull/1858) +* [FEATURE] Logs: Add support for global attributes on logs. See [#1900](https://github.com/DataDog/dd-sdk-android/pull/1900) +* [FEATURE] RUM: Allow setting custom error fingerprint. See [#1911](https://github.com/DataDog/dd-sdk-android/pull/1911) +* [FEATURE] RUM: Report all threads for non-fatal ANRs. See [#1912](https://github.com/DataDog/dd-sdk-android/pull/1912) +* [FEATURE] RUM: Report fatal ANRs. See [#1909](https://github.com/DataDog/dd-sdk-android/pull/1909) +* [BUGFIX] Session Replay: Avoid crash when `applicationContext` is `null`. See [#1864](https://github.com/DataDog/dd-sdk-android/pull/1864) +* [BUGFIX] Session Replay: Fix image resizing issue. See [#1897](https://github.com/DataDog/dd-sdk-android/pull/1897) +* [BUGFIX] Fix typo in source type. See [#1904](https://github.com/DataDog/dd-sdk-android/pull/1904) +* [BUGFIX] RUM: Prevent `ConcurrentModificationException` when reading feature flags. See [#1925](https://github.com/DataDog/dd-sdk-android/pull/1925) +* [IMPROVEMENT] RUM: Disable non-fatal ANR reporting by default. See [#1914](https://github.com/DataDog/dd-sdk-android/pull/1914) +* [IMPROVEMENT] RUM: Introduce `error.category` attribute for exceptions, categorize ANRs separately. See [#1918](https://github.com/DataDog/dd-sdk-android/pull/1918) +* [MAINTENANCE] Next dev iteration. See [#1861](https://github.com/DataDog/dd-sdk-android/pull/1861) +* [MAINTENANCE] Merge `release/2.6.0` in `develop`. See [#1862](https://github.com/DataDog/dd-sdk-android/pull/1862) +* [MAINTENANCE] Merge `release/2.6.1` changes into `develop` branch. See [#1868](https://github.com/DataDog/dd-sdk-android/pull/1868) +* [MAINTENANCE] Update telemetry schema. See [#1874](https://github.com/DataDog/dd-sdk-android/pull/1874) +* [MAINTENANCE] Merge Hotfix 2.6.2. See [#1890](https://github.com/DataDog/dd-sdk-android/pull/1890) +* [MAINTENANCE] Add signed commits requirement to `CONTRIBUTING.md`. See [#1905](https://github.com/DataDog/dd-sdk-android/pull/1905) +* [MAINTENANCE] Session Replay: Cleanup SR code. See [#1910](https://github.com/DataDog/dd-sdk-android/pull/1910) +* [MAINTENANCE] Session Replay: Fix integration tests post Session Replay refactoring. See [#1916](https://github.com/DataDog/dd-sdk-android/pull/1916) +* [MAINTENANCE] Session Replay: Fix `SrImageButtonsMaskUserInputTest`. See [#1920](https://github.com/DataDog/dd-sdk-android/pull/1920) +* [MAINTENANCE] Adjust `ktlint` formatting rules. See [#1919](https://github.com/DataDog/dd-sdk-android/pull/1919) +* [MAINTENANCE] Fix formatting. See [#1921](https://github.com/DataDog/dd-sdk-android/pull/1921) + +# 2.6.2 / 2024-02-23 + +* [BUGFIX] RUM: Fix crash in frame rate vital detection. See [#1872](https://github.com/DataDog/dd-sdk-android/pull/1872) + +# 2.6.1 / 2024-02-21 + +* [BUGFIX] RUM: Fix missing source in telemetry json schema. See [#1865](https://github.com/DataDog/dd-sdk-android/pull/1865) +* [MAINTENANCE] RUM: Remove stale json schema file. See [#1866](https://github.com/DataDog/dd-sdk-android/pull/1866) + +# 2.6.0 / 2024-02-19 + +* [FEATURE] RUM\Logs: Report all threads in case of crash. See [#1848](https://github.com/DataDog/dd-sdk-android/pull/1848) +* [BUGFIX] RUM: Make a copy of attributes before passing them to RUM event. See [#1830](https://github.com/DataDog/dd-sdk-android/pull/1830) +* [BUGFIX] Session Replay: Add traversal flag to snapshot items. See [#1837](https://github.com/DataDog/dd-sdk-android/pull/1837) +* [BUGFIX] Drop batch telemetry where duration or age have negative values. See [#1850](https://github.com/DataDog/dd-sdk-android/pull/1850) +* [BUGFIX] RUM: Do not update RUM View global properties after the view is stopped. See [#1851](https://github.com/DataDog/dd-sdk-android/pull/1851) +* [IMPROVEMENT] RUM: Improve vital support for higher refresh rate devices. See [#1806](https://github.com/DataDog/dd-sdk-android/pull/1806) +* [IMPROVEMENT] RUM: Add more HTTP methods to RUM. See [#1826](https://github.com/DataDog/dd-sdk-android/pull/1826) +* [IMPROVEMENT] RUM: Start session when RUM is initialized. See [#1832](https://github.com/DataDog/dd-sdk-android/pull/1832) +* [IMPROVEMENT] RUM: Add new error source types to RUM schema. See [#1855](https://github.com/DataDog/dd-sdk-android/pull/1855) +* [IMPROVEMENT] RUM: Set `source_type` on native crashes to `ndk`. See [#1856](https://github.com/DataDog/dd-sdk-android/pull/1856) +* [MAINTENANCE] Next dev iteration 2.6.0. See [#1823](https://github.com/DataDog/dd-sdk-android/pull/1823) +* [MAINTENANCE] Merge `release/2.5.0` branch into `develop` branch. See [#1825](https://github.com/DataDog/dd-sdk-android/pull/1825) +* [MAINTENANCE] Update RUM Schema. See [#1828](https://github.com/DataDog/dd-sdk-android/pull/1828) +* [MAINTENANCE] Merge 2.5.1 into develop. See [#1842](https://github.com/DataDog/dd-sdk-android/pull/1842) +* [MAINTENANCE] Introduce github issue forms. See [#1852](https://github.com/DataDog/dd-sdk-android/pull/1852) + +# 2.5.1 / 2024-01-24 + +* [BUGFIX] RUM: Prevent crash due to concurrent modification of custom attributes. See [#1838](https://github.com/DataDog/dd-sdk-android/pull/1838) + +# 2.5.0 / 2024-01-15 + +* [FEATURE] Add accessor for current session id. See [#1810](https://github.com/DataDog/dd-sdk-android/pull/1810) +* [BUGFIX] Session Replay: Enable recording session if first RUM message happened before init. See [#1777](https://github.com/DataDog/dd-sdk-android/pull/1777) +* [BUGFIX] RUM: Fix view url in case of `NavigationViewTrackingStrategy` usage. See [#1791](https://github.com/DataDog/dd-sdk-android/pull/1791) +* [BUGFIX] Session Replay: Fix `ConcurrentModificationException` in `BitmapPool`. See [#1798](https://github.com/DataDog/dd-sdk-android/pull/1798) +* [BUGFIX] RUM: Use internal key for View Scopes. See [#1812](https://github.com/DataDog/dd-sdk-android/pull/1812) +* [BUGFIX] `getCurrentSessionId` returns correct value. See [#1817](https://github.com/DataDog/dd-sdk-android/pull/1817) +* [IMPROVEMENT] RUM: Better handling of event write errors in RUM. See [#1766](https://github.com/DataDog/dd-sdk-android/pull/1766) +* [IMPROVEMENT] Single Feature Integration Tests: Trace. See [#1786](https://github.com/DataDog/dd-sdk-android/pull/1786) +* [IMPROVEMENT] Optimize response body length reporting in OkHttp instrumentation. See [#1790](https://github.com/DataDog/dd-sdk-android/pull/1790) +* [IMPROVEMENT] RUM/Synthetics: Make synthetics logs more verbose. See [#1813](https://github.com/DataDog/dd-sdk-android/pull/1813) +* [IMPROVEMENT] Prevent false positive warning. See [#1815](https://github.com/DataDog/dd-sdk-android/pull/1815) +* [IMPROVEMENT] RUM: Safe serialization of user-provided attributes. See [#1818](https://github.com/DataDog/dd-sdk-android/pull/1818) +* [IMPROVEMENT] RUM: Add additional status codes as retryable. See [#1819](https://github.com/DataDog/dd-sdk-android/pull/1819) +* [MAINTENANCE] Merge `release/2.4.0` into `develop` branch. See [#1784](https://github.com/DataDog/dd-sdk-android/pull/1784) +* [MAINTENANCE] Add delay before executors are flushed and stopped in nightly tests. See [#1783](https://github.com/DataDog/dd-sdk-android/pull/1783) +* [MAINTENANCE] Upgrade shared CI pipeline. See [#1789](https://github.com/DataDog/dd-sdk-android/pull/1789) +* [MAINTENANCE] Add telemetry point for null file content. See [#1792](https://github.com/DataDog/dd-sdk-android/pull/1792) +* [MAINTENANCE] RUM: Migrate Realm from KAPT to KSP. See [#1794](https://github.com/DataDog/dd-sdk-android/pull/1794) +* [MAINTENANCE] Fix OOM in CI jobs. See [#1796](https://github.com/DataDog/dd-sdk-android/pull/1796) +* [MAINTENANCE] Increase wait time in NightlyTestRule. See [#1799](https://github.com/DataDog/dd-sdk-android/pull/1799) +* [MAINTENANCE] Use Datadog Agent 1.26.1. See [#1800](https://github.com/DataDog/dd-sdk-android/pull/1800) +* [MAINTENANCE] Move reading `BUILDENV_HOST_IP` variable to the job script definition. See [#1801](https://github.com/DataDog/dd-sdk-android/pull/1801) +* [MAINTENANCE] Use automatic Gradle daemon instrumentation with CI Visibility instead of manual test tasks instrumentation. See [#1804](https://github.com/DataDog/dd-sdk-android/pull/1804) + +# 2.4.0 / 2023-12-21 + +* [FEATURE] Global: Create `PersistenceStrategy` interface. See [#1745](https://github.com/DataDog/dd-sdk-android/pull/1745) +* [FEATURE] Global: Let customer set custom persistence strategy in configuration. See [#1746](https://github.com/DataDog/dd-sdk-android/pull/1746) +* [FEATURE] Global: Implement `AbstractStorage`. See [#1747](https://github.com/DataDog/dd-sdk-android/pull/1747) +* [FEATURE] Global: Use `AbstractStorage` when custom persistence strategy provided. See [#1748](https://github.com/DataDog/dd-sdk-android/pull/1748) +* [FEATURE] RUM: Print RUM app, session and view ID in LogCat. See [#1760](https://github.com/DataDog/dd-sdk-android/pull/1760) +* [BUGFIX] Session Replay: Fix duplicate wireframes issue. See [#1761](https://github.com/DataDog/dd-sdk-android/pull/1761) +* [BUGFIX] Global: Fix `ConcurrentModificationException` during `ConsentAwareStorage.dropAll` call. See [#1764](https://github.com/DataDog/dd-sdk-android/pull/1764) +* [BUGFIX] RUM: Convert pending resource to pending error when Resource scope completes with an error. See [#1776](https://github.com/DataDog/dd-sdk-android/pull/1776) +* [BUGFIX] RUM: Fix leak caused by repeated calls to `WeakReference.get()`. See [#1779](https://github.com/DataDog/dd-sdk-android/pull/1779) +* [IMPROVEMENT] Session Replay: Add `resourceId` to `ImageWireframe`. See [#1690](https://github.com/DataDog/dd-sdk-android/pull/1690) +* [IMPROVEMENT] `Logger` integration tests. See [#1735](https://github.com/DataDog/dd-sdk-android/pull/1735) +* [IMPROVEMENT] Add regression test for `Gson#toString` method. See [#1742](https://github.com/DataDog/dd-sdk-android/pull/1742) +* [IMPROVEMENT] Create Stub Core module. See [#1740](https://github.com/DataDog/dd-sdk-android/pull/1740) +* [IMPROVEMENT] Fix flaky test in `WireframeUtils`. See [#1743](https://github.com/DataDog/dd-sdk-android/pull/1743) +* [IMPROVEMENT] Session Replay: Remove `resourceId` field from e2e payloads. See [#1754](https://github.com/DataDog/dd-sdk-android/pull/1754) +* [IMPROVEMENT] RUM: Add session start reason to events. See [#1755](https://github.com/DataDog/dd-sdk-android/pull/1755) +* [IMPROVEMENT] Session Replay: Open text masking classes for extension. See [#1757](https://github.com/DataDog/dd-sdk-android/pull/1757) +* [IMPROVEMENT] Tracing: Update RUM attributes in spans. See [#1758](https://github.com/DataDog/dd-sdk-android/pull/1758) +* [IMPROVEMENT] Add the synchronous equivalent of `readNextBatch` and `confirmBatchRead` in Storage API. See [#1768](https://github.com/DataDog/dd-sdk-android/pull/1768) +* [IMPROVEMENT] Add all Logs Feature integration tests. See [#1769](https://github.com/DataDog/dd-sdk-android/pull/1769) +* [IMPROVEMENT] Remove the v1 data upload components. See [#1774](https://github.com/DataDog/dd-sdk-android/pull/1774) +* [IMPROVEMENT] Add text overflow examples in sample app. See [#1775](https://github.com/DataDog/dd-sdk-android/pull/1775) +* [IMPROVEMENT] Remove data store/upload config from feature configuration. See [#1778](https://github.com/DataDog/dd-sdk-android/pull/1778) +* [MAINTENANCE] Bump dev version to 2.4.0. See [#1738](https://github.com/DataDog/dd-sdk-android/pull/1738) +* [MAINTENANCE] Merge `release/2.3.0` branch into `develop` branch. See [#1739](https://github.com/DataDog/dd-sdk-android/pull/1739) +* [MAINTENANCE] Update RUM schema. See [#1752](https://github.com/DataDog/dd-sdk-android/pull/1752) +* [MAINTENANCE] Remove obsolete integration tests. See [#1770](https://github.com/DataDog/dd-sdk-android/pull/1770) +* [MAINTENANCE] Update obsolete nightly logs test. See [#1771](https://github.com/DataDog/dd-sdk-android/pull/1771) +* [MAINTENANCE] Add artifacts in Gitlab test jobs. See [#1772](https://github.com/DataDog/dd-sdk-android/pull/1772) +* [DOCS] Mention `DatadogTree` in README.md. See [#1744](https://github.com/DataDog/dd-sdk-android/pull/1744) + +# 2.3.0 / 2023-11-21 + +* [FEATURE] Global: Support returning event metadata to the readers. See [#1670](https://github.com/DataDog/dd-sdk-android/pull/1670) +* [FEATURE] Add mapper interface for traversing all children. See [#1684](https://github.com/DataDog/dd-sdk-android/pull/1684) +* [FEATURE] Global: Introduce the `BatchProcessingLevel` API. See [#1686](https://github.com/DataDog/dd-sdk-android/pull/1686) +* [FEATURE] Session Replay: Support `ImageView` views. See [#1677](https://github.com/DataDog/dd-sdk-android/pull/1677) +* [FEATURE] RUM: Create a `SetSyntheticsTestAttribute` event. See [#1714](https://github.com/DataDog/dd-sdk-android/pull/1714) +* [FEATURE] Add synthetics information to the RUM Context. See [#1715](https://github.com/DataDog/dd-sdk-android/pull/1715) +* [FEATURE] Store the synthetics test info in the RUM Context. See [#1716](https://github.com/DataDog/dd-sdk-android/pull/1716) +* [FEATURE] Add synthetics info to RUM Views. See [#1717](https://github.com/DataDog/dd-sdk-android/pull/1717) +* [FEATURE] Add synthetics info to RUM Actions. See [#1718](https://github.com/DataDog/dd-sdk-android/pull/1718) +* [FEATURE] Add synthetics info to RUM Errors. See [#1719](https://github.com/DataDog/dd-sdk-android/pull/1719) +* [FEATURE] Add synthetics info to RUM Resources. See [#1720](https://github.com/DataDog/dd-sdk-android/pull/1720) +* [FEATURE] Add synthetics info to RUM Long Tasks. See [#1721](https://github.com/DataDog/dd-sdk-android/pull/1721) +* [FEATURE] RUM: Track synthetics info from activity extras. See [#1722](https://github.com/DataDog/dd-sdk-android/pull/1722) +* [BUGFIX] Fix the issue of missing cpu/memory info with RUM view events. See [#1693](https://github.com/DataDog/dd-sdk-android/pull/1693) +* [BUGFIX] Fix batch processing level reporting in core configuration telemetry. See [#1698](https://github.com/DataDog/dd-sdk-android/pull/1698) +* [BUGFIX] Unregister RUM monitor when associated RUM feature is stopped. See [#1725](https://github.com/DataDog/dd-sdk-android/pull/1725) +* [BUGFIX] Session Replay: Generate wireframe IDs as 32bit integer. See [#1736](https://github.com/DataDog/dd-sdk-android/pull/1736) +* [IMPROVEMENT] Unit test to confirm Session Replay records order is kept when having same timestamps. See [#1659](https://github.com/DataDog/dd-sdk-android/pull/1659) +* [IMPROVEMENT] Global: Handle Android Strict Mode. See [#1663](https://github.com/DataDog/dd-sdk-android/pull/1663) +* [IMPROVEMENT] Make sure we use try-locks in our NDK signal catcher. See [#1665](https://github.com/DataDog/dd-sdk-android/pull/1665) +* [IMPROVEMENT] RUM: Introduce view event filtering in upload pipeline, remove view event throttling in write pipeline. See [#1678](https://github.com/DataDog/dd-sdk-android/pull/1678) +* [IMPROVEMENT] Make NDK stack traces more standard. See [#1683](https://github.com/DataDog/dd-sdk-android/pull/1683) +* [IMPROVEMENT] Have more consistent results when using the load picture sample screen. See [#1692](https://github.com/DataDog/dd-sdk-android/pull/1692) +* [IMPROVEMENT] Add the `batchProcessingLevel` value to the Configuration Telemetry. See [#1691](https://github.com/DataDog/dd-sdk-android/pull/1691) +* [IMPROVEMENT] Tracing: Update default propagation style from `Datadog` to `Datadog`+`TraceContext`. See [#1696](https://github.com/DataDog/dd-sdk-android/pull/1696) +* [IMPROVEMENT] Tracing: Use `tracestate` header to supply vendor-specific information. See [#1694](https://github.com/DataDog/dd-sdk-android/pull/1694) +* [IMPROVEMENT] Global: Lower the upload frequency and batch size enum values. See [#1733](https://github.com/DataDog/dd-sdk-android/pull/1733) +* [MAINTENANCE] Prepare release 2.2.0. See [#1650](https://github.com/DataDog/dd-sdk-android/pull/1650) +* [MAINTENANCE] Next dev iteration. See [#1651](https://github.com/DataDog/dd-sdk-android/pull/1651) +* [MAINTENANCE] Merge release 2.2.0 branch into develop. See [#1657](https://github.com/DataDog/dd-sdk-android/pull/1657) +* [MAINTENANCE] Fix Session Replay functional tests payloads after develop rollback. See [#1660](https://github.com/DataDog/dd-sdk-android/pull/1660) +* [MAINTENANCE] Create core `testFixtures` source set. See [#1666](https://github.com/DataDog/dd-sdk-android/pull/1666) +* [MAINTENANCE] Refactor shared android library build script. See [#1667](https://github.com/DataDog/dd-sdk-android/pull/1667) +* [MAINTENANCE] Let all modules use the shared fixtures. See [#1668](https://github.com/DataDog/dd-sdk-android/pull/1668) +* [MAINTENANCE] Update testing conventions. See [#1661](https://github.com/DataDog/dd-sdk-android/pull/1661) +* [MAINTENANCE] Disable warning as errors locally. See [#1664](https://github.com/DataDog/dd-sdk-android/pull/1664) +* [MAINTENANCE] Add test pyramid scaffolding. See [#1674](https://github.com/DataDog/dd-sdk-android/pull/1674) +* [MAINTENANCE] Share `RawBatchEvent` forgery for tests between the modules. See [#1680](https://github.com/DataDog/dd-sdk-android/pull/1680) +* [MAINTENANCE] Calculate API coverage. See [#1681](https://github.com/DataDog/dd-sdk-android/pull/1681) +* [MAINTENANCE] Improve `LogsFragment` in sample app. See [#1685](https://github.com/DataDog/dd-sdk-android/pull/1685) +* [MAINTENANCE] Add CI task to update E2E sample app. See [#1688](https://github.com/DataDog/dd-sdk-android/pull/1688) +* [MAINTENANCE] Include `rum-mobile-android` as codeowner. See [#1695](https://github.com/DataDog/dd-sdk-android/pull/1695) +* [MAINTENANCE] Fix flaky test in `WebViewRumEventConsumerTest`. See [#1724](https://github.com/DataDog/dd-sdk-android/pull/1724) +* [MAINTENANCE] Fix flaky test in RumEventDeserializer. See [#1727](https://github.com/DataDog/dd-sdk-android/pull/1727) +* [MAINTENANCE] Fix flaky test in DatadogContextProvider. See [#1726](https://github.com/DataDog/dd-sdk-android/pull/1726) +* [MAINTENANCE] Fix flaky test in `TelemetryEventHandlerTest`. See [#1729](https://github.com/DataDog/dd-sdk-android/pull/1729) +* [MAINTENANCE] Fix flaky test in `BatchFileOrchestratorTest`. See [#1732](https://github.com/DataDog/dd-sdk-android/pull/1732) +* [MAINTENANCE] Reduce noise in logs when building the project. See [#1731](https://github.com/DataDog/dd-sdk-android/pull/1731) +* [MAINTENANCE] Fix flaky test in `MainLooperLongTaskStrategyTest`. See [#1730](https://github.com/DataDog/dd-sdk-android/pull/1730) +* [MAINTENANCE] Create `SDKCore` stub classes. See [#1734](https://github.com/DataDog/dd-sdk-android/pull/1734) + +# 2.2.0 / 2023-10-04 + +* [FEATURE] Session Replay: Serialize TextViews/Buttons to base64. See [#1592](https://github.com/DataDog/dd-sdk-android/pull/1592) +* [FEATURE] WebView Tracking: Add sampler to `WebViewLogEventConsumer`. See [#1629](https://github.com/DataDog/dd-sdk-android/pull/1629) +* [FEATURE] RUM: Add cross platform GraphQL attributes. See [#1631](https://github.com/DataDog/dd-sdk-android/pull/1631) +* [FEATURE] Trace: Add `networkInfoEnabled` option in `TraceConfiguration`. See [#1636](https://github.com/DataDog/dd-sdk-android/pull/1636) +* [FEATURE] Logs: Add `isEnabled` to Logs. See [#1648](https://github.com/DataDog/dd-sdk-android/pull/1648) +* [BUGFIX] Session Replay: Fix `RippleDrawables`. See [#1600](https://github.com/DataDog/dd-sdk-android/pull/1600) +* [BUGFIX] Session Replay: Fix Base64 issues with multithreading. See [#1613](https://github.com/DataDog/dd-sdk-android/pull/1613) +* [BUGFIX] RUM: Treat scroll on non-scrollable view as tap. See [#1622](https://github.com/DataDog/dd-sdk-android/pull/1622) +* [BUGFIX] Trace: Fix `PendingTrace` `ConcurrentModificationException`. See [#1623](https://github.com/DataDog/dd-sdk-android/pull/1623) +* [BUGFIX] RUM: Propagate session state and view type as Strings. See [#1625](https://github.com/DataDog/dd-sdk-android/pull/1625) +* [BUGFIX] Fix the WebView fragment in sample app. See [#1627](https://github.com/DataDog/dd-sdk-android/pull/1627) +* [BUGFIX] RUM: Prevent NPE in `GestureListener`. See [#1634](https://github.com/DataDog/dd-sdk-android/pull/1634) +* [BUGFIX] RUM: Fix duplicate views in `MixedViewTrackingStrategy`. See [#1639](https://github.com/DataDog/dd-sdk-android/pull/1639) +* [BUGFIX] Telemetry: Fix the batch duration value in `batch_close` telemetry event. See [#1633](https://github.com/DataDog/dd-sdk-android/pull/1633) +* [BUGFIX] Global: Make `FeatureFileOrchesrator` use file persistence config created from user/feature settings. See [#1643](https://github.com/DataDog/dd-sdk-android/pull/1643) +* [BUGFIX] Telemetry: Fix RegEx in `FeatureFileOrchestrator` to resolve file consent type. See [#1645](https://github.com/DataDog/dd-sdk-android/pull/1645) +* [IMPROVEMENT] Session Replay: Base64 Caching Mechanism. See [#1534](https://github.com/DataDog/dd-sdk-android/pull/1534) +* [IMPROVEMENT] Session Replay: Implement bitmap downscaling. See [#1546](https://github.com/DataDog/dd-sdk-android/pull/1546) +* [IMPROVEMENT] Session Replay: Implement pool of reusable bitmaps. See [#1554](https://github.com/DataDog/dd-sdk-android/pull/1554) +* [IMPROVEMENT] Session Replay: Refactor caches from singletons to class instances. See [#1564](https://github.com/DataDog/dd-sdk-android/pull/1564) +* [IMPROVEMENT] Session Replay: Optimize bitmap processing. See [#1576](https://github.com/DataDog/dd-sdk-android/pull/1576) +* [IMPROVEMENT] Session Replay: Add the Session Replay functional tests for sensitive input fields. See [#1601](https://github.com/DataDog/dd-sdk-android/pull/1601) +* [IMPROVEMENT] Session Replay: Add the Session Replay functional tests for checkboxes and radiobuttons. See [#1609](https://github.com/DataDog/dd-sdk-android/pull/1609) +* [IMPROVEMENT] Add sample showing how to listen to memory events. See [#1621](https://github.com/DataDog/dd-sdk-android/pull/1621)) +* [IMPROVEMENT] WebView Tracking: Only send Webview RUM events when Native Session exists and is tracked. See [#1626](https://github.com/DataDog/dd-sdk-android/pull/1626) +* [IMPROVEMENT] Session Replay: Fix the async image loading logic inside the Session Replay view mappers. See [#1619](https://github.com/DataDog/dd-sdk-android/pull/1619) +* [IMPROVEMENT] RUM: Let exceptions from `Window.Callback` to propagate. See [#1632](https://github.com/DataDog/dd-sdk-android/pull/1632) +* [IMPROVEMENT] Session Replay: Add Session Replay functional tests for `ImageButtons` and `ImageViews`. See [#1630](https://github.com/DataDog/dd-sdk-android/pull/1630) +* [IMPROVEMENT] Trace: Make network info optional in span schema. See [#1635](https://github.com/DataDog/dd-sdk-android/pull/1635) +* [IMPROVEMENT] Trace: Use `networkInfoEnabled` to serialize or not network info in spans. See [#1637](https://github.com/DataDog/dd-sdk-android/pull/1637) +* [IMPROVEMENT] Telemetry: Add more information into the batch telemetry. See [#1641](https://github.com/DataDog/dd-sdk-android/pull/1641) +* [IMPROVEMENT] Session Replay: Implement heuristic image classification. See [#1640](https://github.com/DataDog/dd-sdk-android/pull/1640) +* [IMPROVEMENT] Global: `DataUploadWorker` is scheduled every time and on non-roaming network. See [#1647](https://github.com/DataDog/dd-sdk-android/pull/1647) +* [IMPROVEMENT] RUM: Use enum for HTTP method parameter of `RumMonitor#startResource API`. See [#1653](https://github.com/DataDog/dd-sdk-android/pull/1653) +* [MAINTENANCE] Align the Base64 feature branch with develop. See [#1594](https://github.com/DataDog/dd-sdk-android/pull/1594) +* [MAINTENANCE] Integrate latest changes from develop into base64 feature. See [#1599](https://github.com/DataDog/dd-sdk-android/pull/1599) +* [MAINTENANCE] Base64 feature branch integration. See [#1597](https://github.com/DataDog/dd-sdk-android/pull/1597) +* [MAINTENANCE] Session Replay: Implement the Session Replay payloads update `local_ci` task. See [#1598](https://github.com/DataDog/dd-sdk-android/pull/1598) +* [MAINTENANCE] Fix Android Studio 'Rebuild Project'. See [#1602](https://github.com/DataDog/dd-sdk-android/pull/1602) +* [MAINTENANCE] Next dev iteration 2.2.0. See [#1604](https://github.com/DataDog/dd-sdk-android/pull/1604) +* [MAINTENANCE] Merge release 2.1.0 into develop branch. See [#1607](https://github.com/DataDog/dd-sdk-android/pull/1607) +* [MAINTENANCE] Use shared Android Lint check. See [#1608](https://github.com/DataDog/dd-sdk-android/pull/1608) +* [MAINTENANCE] Provide session replay data in configuration telemetry. See [#1611](https://github.com/DataDog/dd-sdk-android/pull/1611) +* [MAINTENANCE] Fix unit test issues caused by git merge. See [#1618](https://github.com/DataDog/dd-sdk-android/pull/1618) +* [MAINTENANCE] Session Replay: Update functional tests due to `ImageView` support. See [#1646](https://github.com/DataDog/dd-sdk-android/pull/1646) +* [MAINTENANCE] Target Android 14 (API 34). See [#1649](https://github.com/DataDog/dd-sdk-android/pull/1649) + +# 2.1.0 / 2023-09-07 + +* [BUGFIX] Session Replay: Do not resolve `WindowManager` from `Application` context. See [#1558](https://github.com/DataDog/dd-sdk-android/pull/1558) +* [BUGFIX] RUM: Report `ApplicationLaunch` view even if first RUM event is not interaction. See [#1591](https://github.com/DataDog/dd-sdk-android/pull/1591) +* [BUGFIX] RUM: Fix crash when disabling `JankStats` tracking. See [#1596](https://github.com/DataDog/dd-sdk-android/pull/1596) +* [IMPROVEMENT] RUM: Add sample rate to reported RUM events. See [#1566](https://github.com/DataDog/dd-sdk-android/pull/1566) +* [IMPROVEMENT] Session Replay: Remove the query parameters from SR requests. See [#1568](https://github.com/DataDog/dd-sdk-android/pull/1568) +* [IMPROVEMENT] Session Replay: Use `internalLogger` in SR modules. See [#1574](https://github.com/DataDog/dd-sdk-android/pull/1574) +* [IMPROVEMENT] Add the `additionalProperties` capability to telemetry debug log event. See [#1575](https://github.com/DataDog/dd-sdk-android/pull/1575) +* [IMPROVEMENT] Global: Collect the `batch_deleted` telemetry. See [#1577](https://github.com/DataDog/dd-sdk-android/pull/1577) +* [IMPROVEMENT] RUM: Fix view tracking gap. See [#1578](https://github.com/DataDog/dd-sdk-android/pull/1578) +* [IMPROVEMENT] Fix tests around `InternalLogger`. See [#1579](https://github.com/DataDog/dd-sdk-android/pull/1579) +* [IMPROVEMENT] Introduce the new `InternalLogger#metric` API. See [#1581](https://github.com/DataDog/dd-sdk-android/pull/1581) +* [IMPROVEMENT] Global: Collect the `batch_closed` telemetry. See [#1586](https://github.com/DataDog/dd-sdk-android/pull/1586) +* [IMPROVEMENT] Add multiple instance sample. See [#1587](https://github.com/DataDog/dd-sdk-android/pull/1587) +* [IMPROVEMENT] Global: Provide the `inBackground` property for `batch_delete` metric. See [#1588](https://github.com/DataDog/dd-sdk-android/pull/1588) +* [IMPROVEMENT] Global: Unregister process lifecycle monitor in core instance stop. See [#1589](https://github.com/DataDog/dd-sdk-android/pull/1589) +* [IMPROVEMENT] Session Replay: Add SR integration tests for `TextView` and `EditText` view type. See [#1593](https://github.com/DataDog/dd-sdk-android/pull/1593) +* [MAINTENANCE] Mention Datadog SDK explicitly in dogfood script. See [#1557](https://github.com/DataDog/dd-sdk-android/pull/1557) +* [MAINTENANCE] Remove redundant `sqlite` product flavour folder in the sample app. See [#1559](https://github.com/DataDog/dd-sdk-android/pull/1559) +* [MAINTENANCE] Next dev cycle 2.1.0. See [#1562](https://github.com/DataDog/dd-sdk-android/pull/1562) +* [MAINTENANCE] Remove the bridge dogfooding step, bridge repo is archived. See [#1571](https://github.com/DataDog/dd-sdk-android/pull/1571) +* [MAINTENANCE] Update gitlab CI env variables for gitlab 16. See [#1595](https://github.com/DataDog/dd-sdk-android/pull/1595) +* [DOCS] Session Replay: Add the `README` files for SR modules. See [#1567](https://github.com/DataDog/dd-sdk-android/pull/1567) + +# 2.0.0 / 2023-07-31 + +This is the first official production version of SDK v2 containing the new architecture for features initialisation and dependencies distribution. See the [migration guide](https://github.com/DataDog/dd-sdk-android/blob/62aac79c3c68c4da02c96ab1071fb5e63f1b8b89/MIGRATION.MD) for details. + +Below you can find the change logs in comparison with out last stable version `1.19.3`: + +* [FEATURE] RUM: Introduce Mobile Session Replay (in Beta). +* [IMPROVEMENT] RUM: Remove tracking of view loading time and fix unit tests. See [#1545](https://github.com/DataDog/dd-sdk-android/pull/1545) +* [IMPROVEMENT] Don't report OkHttp throwables to telemetry. See [#1548](https://github.com/DataDog/dd-sdk-android/pull/1548) +* [IMPROVEMENT] Use `implementation` dependency for features in integrations modules. See [#1552](https://github.com/DataDog/dd-sdk-android/pull/1552) +* [IMPROVEMENT] Remove `dd-sdk-android-ktx` module. See [#1555](https://github.com/DataDog/dd-sdk-android/pull/1555) +* [BUGFIX] RUM: Fix memory leak in `JankStats` usage. See [#1553](https://github.com/DataDog/dd-sdk-android/pull/1553) +* [DOCS] Remove redundant docs. See [#1540](https://github.com/DataDog/dd-sdk-android/pull/1540) +* [DOCS] Update documentation for SDK v2. See [#1549](https://github.com/DataDog/dd-sdk-android/pull/1549) +* [IMPROVEMENT] Global: Provide the `uploadFrequency` per feature. See [#1533](https://github.com/DataDog/dd-sdk-android/pull/1533) +* [IMPROVEMENT] RUM: Update documentation of `ViewEventMapper`. See [#1537](https://github.com/DataDog/dd-sdk-android/pull/1537) +* [IMPROVEMENT] RUM: Support `navHost` hosted by `FragmentContainerView` for `NavigationViewTrackingStrategy`. See [#1538](https://github.com/DataDog/dd-sdk-android/pull/1538) +* [IMPROVEMENT] Session Replay: Always listen and react to RUM session state. See [#1539](https://github.com/DataDog/dd-sdk-android/pull/1539) +* [IMPROVEMENT] Session Replay: Remove explicit dependency on RUM module from Session Replay. See [#1541](https://github.com/DataDog/dd-sdk-android/pull/1541) +* [IMPROVEMENT] Update Compose Navigation version to 2.6.0. See [#1542](https://github.com/DataDog/dd-sdk-android/pull/1542) +* [IMPROVEMENT] Don't reexport `OkHttp` from `Glide`. See [#1543](https://github.com/DataDog/dd-sdk-android/pull/1543) +* [IMPROVEMENT] Introduce known file cache and cleanup throttling in `BatchFileOrchestrator` in order to reduce the number of syscalls. See [#1506](https://github.com/DataDog/dd-sdk-android/pull/1506) +* [IMPROVEMENT] Logs: Alter public API of `Logger` to receive `Any` data type. See [#1324](https://github.com/DataDog/dd-sdk-android/pull/1324) +* [IMPROVEMENT] RUM: Use `JankStats` for FPS measuring. See [#1405](https://github.com/DataDog/dd-sdk-android/pull/1405) +* [IMPROVEMENT] RUM: Fix `JankStats` usage. See [#1512](https://github.com/DataDog/dd-sdk-android/pull/1512) +* [BUGFIX] RUM: Keep old `viewId`s for view scope. See [#1448](https://github.com/DataDog/dd-sdk-android/pull/1448) + +# 2.0.0-beta3 / 2023-07-26 + +This is a beta release of SDK v2. Compared to SDK v1 it contains breaking changes related to the SDK setup and APIs. See the [migration guide](https://github.com/DataDog/dd-sdk-android/blob/5c9feb900856a6d7b3623820dade1eaead1498b9/CHANGELOG.md) for details. + +Changes in comparison with `2.0.0-beta2`: + +* [IMPROVEMENT] RUM: Remove tracking of view loading time and fix unit tests. See [#1545](https://github.com/DataDog/dd-sdk-android/pull/1545) +* [IMPROVEMENT] Don't report OkHttp throwables to telemetry. See [#1548](https://github.com/DataDog/dd-sdk-android/pull/1548) +* [IMPROVEMENT] Use `implementation` dependency for features in integrations modules. See [#1552](https://github.com/DataDog/dd-sdk-android/pull/1552) +* [IMPROVEMENT] Remove `dd-sdk-android-ktx` module. See [#1555](https://github.com/DataDog/dd-sdk-android/pull/1555) +* [BUGFIX] RUM: Fix memory leak in `JankStats` usage. See [#1553](https://github.com/DataDog/dd-sdk-android/pull/1553) +* [DOCS] Remove redundant docs. See [#1540](https://github.com/DataDog/dd-sdk-android/pull/1540) +* [DOCS] Update documentation for SDK v2. See [#1549](https://github.com/DataDog/dd-sdk-android/pull/1549) + +# 2.0.0-beta2 / 2023-07-17 + +This is a beta release of SDK v2. Compared to SDK v1 it contains breaking changes related to the SDK setup and APIs. See the [migration guide](https://github.com/DataDog/dd-sdk-android/blob/8d1f9abb101039abcd44ffed2823655c33e5129f/MIGRATION.MD) for details. + +Changes in comparison with `2.0.0-beta1`: + +* [IMPROVEMENT] Global: Provide the `uploadFrequency` per feature. See [#1533](https://github.com/DataDog/dd-sdk-android/pull/1533) +* [IMPROVEMENT] RUM: Update documentation of `ViewEventMapper`. See [#1537](https://github.com/DataDog/dd-sdk-android/pull/1537) +* [IMPROVEMENT] RUM: Support `navHost` hosted by `FragmentContainerView` for `NavigationViewTrackingStrategy`. See [#1538](https://github.com/DataDog/dd-sdk-android/pull/1538) +* [IMPROVEMENT] Session Replay: Always listen and react to RUM session state. See [#1539](https://github.com/DataDog/dd-sdk-android/pull/1539) +* [IMPROVEMENT] Session Replay: Remove explicit dependency on RUM module from Session Replay. See [#1541](https://github.com/DataDog/dd-sdk-android/pull/1541) +* [IMPROVEMENT] Update Compose Navigation version to 2.6.0. See [#1542](https://github.com/DataDog/dd-sdk-android/pull/1542) +* [IMPROVEMENT] Don't reexport `OkHttp` from `Glide`. See [#1543](https://github.com/DataDog/dd-sdk-android/pull/1543) + +# 1.19.3 / 2023-07-11 + +* [IMPROVEMENT] RUM: Introduce known file cache and cleanup throttling in `BatchFileOrchestrator` in order to reduce the number of syscalls. See [#1506](https://github.com/DataDog/dd-sdk-android/pull/1506) + +# 2.0.0-beta1 / 2023-07-07 + +This is the first release of SDK v2. It contains breaking changes related to the SDK setup and APIs. See the [migration guide](https://github.com/DataDog/dd-sdk-android/blob/026fc30f5c28226b244a0f6884841cbcac9c864b/MIGRATION.MD) for details. + +Functional changes in comparison with `1.19.2`: + +* [IMPROVEMENT] Introduce known file cache and cleanup throttling in `BatchFileOrchestrator` in order to reduce the number of syscalls. See [#1506](https://github.com/DataDog/dd-sdk-android/pull/1506) +* [IMPROVEMENT] Logs: Alter public API of `Logger` to receive `Any` data type. See [#1324](https://github.com/DataDog/dd-sdk-android/pull/1324) +* [IMPROVEMENT] RUM: Use `JankStats` for FPS measuring. See [#1405](https://github.com/DataDog/dd-sdk-android/pull/1405) +* [IMPROVEMENT] RUM: Fix `JankStats` usage. See [#1512](https://github.com/DataDog/dd-sdk-android/pull/1512) +* [BUGFIX] RUM: Keep old `viewId`s for view scope. See [#1448](https://github.com/DataDog/dd-sdk-android/pull/1448) + +# 1.19.2 / 2023-06-05 + +* [REVERT] RUM: Force new session at SDK initialization. See [#1399](https://github.com/DataDog/dd-sdk-android/pull/1399) + +# 1.19.1 / 2023-05-30 + +* [IMPROVEMENT] RUM: Force new session at SDK initialization. See [#1399](https://github.com/DataDog/dd-sdk-android/pull/1399) +* [BUGFIX] RUM: Ignore adding custom timings and feature flags for the stopped view. See [#1433](https://github.com/DataDog/dd-sdk-android/pull/1433) + +# 1.19.0 / 2023-04-24 + +* [FEATURE] RUM: Allow users to stop a RUM session. See [#1356](https://github.com/DataDog/dd-sdk-android/pull/1356) +* [FEATURE] APM: Add tracer sampling rate. See [#1393](https://github.com/DataDog/dd-sdk-android/pull/1393) +* [IMPROVEMENT] Create a minimal WearOS sample to test compatibility. See [#1384](https://github.com/DataDog/dd-sdk-android/pull/1384) +* [BUGFIX] RUM: Fix stopped `RUMViewManager` from being able to start new views. See [#1381](https://github.com/DataDog/dd-sdk-android/pull/1381) +* [MAINTENANCE] Update RUM Event Schema. See [#1383](https://github.com/DataDog/dd-sdk-android/pull/1383) +* [DOCS] Delete referenced docs and update README. See [#1376](https://github.com/DataDog/dd-sdk-android/pull/1376) + +# 1.18.1 / 2023-03-30 + +* [IMPROVEMENT] RUM: Remove extra telemetry sent when detecting refresh rate scale. See [#1358](https://github.com/DataDog/dd-sdk-android/pull/1358) + +# 1.18.0 / 2023-03-21 + +* [FEATURE] RUM: Add `addFeatureFlagEvaluation` function for RUM. See [#1265](https://github.com/DataDog/dd-sdk-android/pull/1265) +* [FEATURE] RUM: Implement webview proxy for cross platform. See [#1290](https://github.com/DataDog/dd-sdk-android/pull/1290) +* [IMPROVEMENT] RUM: Add support to `AP1`. See [#1268](https://github.com/DataDog/dd-sdk-android/pull/1268) +* [IMPROVEMENT] RUM `ApplicationLaunch` logic changes. See [#1278](https://github.com/DataDog/dd-sdk-android/pull/1278) +* [IMPROVEMENT] RUM: Add internal telemetry configuration sampling rate. See [#1310](https://github.com/DataDog/dd-sdk-android/pull/1310) +* [BUGFIX] RUM: Prevent reporting invalid cpu ticks per seconds. See [#1308](https://github.com/DataDog/dd-sdk-android/pull/1308) +* [BUGFIX] RUM: Fix timing of `ApplicationLaunch` view and `application_start` events. See [#1305](https://github.com/DataDog/dd-sdk-android/pull/1305) +* [BUGFIX] RUM: Fix telemetry sampling internal configuration for Flutter. See [#1326](https://github.com/DataDog/dd-sdk-android/pull/1326) +* [MAINTENANCE] Make tests more accurate with url case sensitivity. See [#1263](https://github.com/DataDog/dd-sdk-android/pull/1263) +* [MAINTENANCE] Update E2E tests with valid resource (as to not get 404). See [#1266](https://github.com/DataDog/dd-sdk-android/pull/1266) +* [MAINTENANCE] Update RUM Telemetry event schema to latest. See [#1319](https://github.com/DataDog/dd-sdk-android/pull/1319) +* [MAINTENANCE] Stabilize integration tests. See [#1329](https://github.com/DataDog/dd-sdk-android/pull/1329) +* [MAINTENANCE] Update static analysis dependency. See [#1342](https://github.com/DataDog/dd-sdk-android/pull/1342) +* [DOCS] Fix numbering in Android tracing instructions markdown. See [#1227](https://github.com/DataDog/dd-sdk-android/pull/1227) +* [DOCS] Updated privacy controls in application setup. See [#1281](https://github.com/DataDog/dd-sdk-android/pull/1281) +* [DOCS] Add app variant definition in docs. See [#1296](https://github.com/DataDog/dd-sdk-android/pull/1296) +* [DOCS] Add link explaining how to stop collecting geolocation data. See [#1327](https://github.com/DataDog/dd-sdk-android/pull/1327) +* [DOCS] Note about how to stop collecting geolocation data. See [#1328](https://github.com/DataDog/dd-sdk-android/pull/1328) + +# 1.17.2 / 2023-03-06 + +* [BUGFIX] Global: Handle devices not reported properly their power source. See [#1315](https://github.com/DataDog/dd-sdk-android/pull/1315) +* [BUGFIX] RUM: Detect device's refresh rate with NavigationViewTrackingStrategy. See [#1312](https://github.com/DataDog/dd-sdk-android/pull/1312) + +# 1.17.1 / 2023-02-20 + +* [BUGFIX] RUM: Revert: Detect device's refresh rate from vital monitor. See [#1251](https://github.com/DataDog/dd-sdk-android/pull/1251) +* [BUGFIX] RUM: The `RumEventMapper` checks `ViewEvent`s by reference. See [#1279](https://github.com/DataDog/dd-sdk-android/pull/1279) +* [BUGFIX] Global: Remove `okhttp3.internal` package usage. See [#1288](https://github.com/DataDog/dd-sdk-android/pull/1288) + +# 1.17.0 / 2023-01-30 + +* [FEATURE] Tracing: Allow the usage of OTel headers in distributed tracing. See [#1229](https://github.com/DataDog/dd-sdk-android/pull/1229) +* [IMPROVEMENT] RUM: Remove cross platform duplicates crashes. See [#1215](https://github.com/DataDog/dd-sdk-android/pull/1215) +* [IMPROVEMENT] RUM: Prevent reporting ANR when app is in background. See [#1239](https://github.com/DataDog/dd-sdk-android/pull/1239) +* [BUGFIX] Use lazy initialization of network-related properties in SampleApplication to be able to pick global first party hosts. See [#1218](https://github.com/DataDog/dd-sdk-android/pull/1218) +* [BUGFIX] Detect device's refresh rate from vital monitor. See [#1251](https://github.com/DataDog/dd-sdk-android/pull/1251) +* [SDK v2] Remove unused `PayloadFormat` and `SdkEndpoint` classes from SDK v2 APIs. See [#1161](https://github.com/DataDog/dd-sdk-android/pull/1161) +* [SDK v2] Make a local copy of tags before creating `LogEvent`. See [#1171](https://github.com/DataDog/dd-sdk-android/pull/1171) +* [SDK v2] Remove duplication of `UserInfo` and `NetworkInfo` classes. See [#1170](https://github.com/DataDog/dd-sdk-android/pull/1170) +* [SDK v2] Use message bus to report Java crashes to RUM. See [#1173](https://github.com/DataDog/dd-sdk-android/pull/1173) +* [SDK v2] Add the `forceNewBatch` option into the `FeatureScope`. See [#1174](https://github.com/DataDog/dd-sdk-android/pull/1174) +* [SDK v2] Use message bus to report NDK crashes to RUM. See [#1177](https://github.com/DataDog/dd-sdk-android/pull/1177) +* [SDK v2] Remove site property from `DatadogContext`. See [#1181](https://github.com/DataDog/dd-sdk-android/pull/1181) +* [SDK v2] Delete obsolete feature-specific uploaders. See [#1199](https://github.com/DataDog/dd-sdk-android/pull/1199) +* [SDK v2] Use `InternalLogger`. See [#1200](https://github.com/DataDog/dd-sdk-android/pull/1200) +* [SDK v2] Remove `Companion` objects with non-public member from Public API. See [#1207](https://github.com/DataDog/dd-sdk-android/pull/1207) +* [SDK v2] Send logs for `Span` using message bus. See [#1211](https://github.com/DataDog/dd-sdk-android/pull/1211) +* [SDK v2] `RequestFactory` can throw exceptions. See [#1214](https://github.com/DataDog/dd-sdk-android/pull/1214) +* [SESSION REPLAY] Fix NPE in `ScreenRecorder` when wrapping a null `window.callback`. See [#1164](https://github.com/DataDog/dd-sdk-android/pull/1164) +* [SESSION REPLAY] Handle SR requests through `SessionReplayRequestsFactory`. See [#1176](https://github.com/DataDog/dd-sdk-android/pull/1176) +* [SESSION REPLAY] Provide SR touch data following the new proposed format. See [#1187](https://github.com/DataDog/dd-sdk-android/pull/1187) +* [SESSION REPLAY] Drop the covered wireframes only if top ones have a background. See [#1198](https://github.com/DataDog/dd-sdk-android/pull/1198) +* [SESSION REPLAY] Fix flaky `WireframeUtils` test. See [#1208](https://github.com/DataDog/dd-sdk-android/pull/1208) +* [SESSION REPLAY] Add dialogs recording support. See [#1206](https://github.com/DataDog/dd-sdk-android/pull/1206) +* [SESSION REPLAY] `RequestFactory` allow to throw exceptions. See [#1217](https://github.com/DataDog/dd-sdk-android/pull/1217) +* [SESSION REPLAY] Use case insensitive when checking wireframe background color opacity. See [#1223](https://github.com/DataDog/dd-sdk-android/pull/1223) +* [SESSION REPLAY] Resolve view snapshot background from theme color. See [#1230](https://github.com/DataDog/dd-sdk-android/pull/1230) +* [MAINTENANCE] Merge `develop` branch into SDK v2 branch. See [#1168](https://github.com/DataDog/dd-sdk-android/pull/1168) +* [MAINTENANCE] Next dev version 1.17.0. See [#1190](https://github.com/DataDog/dd-sdk-android/pull/1190) +* [MAINTENANCE] Upgrade detekt pipeline version. See [#1192](https://github.com/DataDog/dd-sdk-android/pull/1192) +* [MAINTENANCE] Update `apiSurface`. See [#1193](https://github.com/DataDog/dd-sdk-android/pull/1193) +* [MAINTENANCE] Fix flaky `rum_rummonitor_add_background_custom_action_with_outcome` nightly test. See [#1195](https://github.com/DataDog/dd-sdk-android/pull/1195) +* [MAINTENANCE] Merge `release/1.16.0` branch into develop branch. See [#1194](https://github.com/DataDog/dd-sdk-android/pull/1194) +* [MAINTENANCE] Merge `develop` branch into SDK v2 branch. See [#1196](https://github.com/DataDog/dd-sdk-android/pull/1196) +* [MAINTENANCE] Fix flaky telemetry test. See [#1197](https://github.com/DataDog/dd-sdk-android/pull/1197) +* [MAINTENANCE] Use latest version of shared pipeline. See [#1201](https://github.com/DataDog/dd-sdk-android/pull/1201) +* [MAINTENANCE] Enable CodeQL analysis. See [#1204](https://github.com/DataDog/dd-sdk-android/pull/1204) +* [MAINTENANCE] Speed up build of sample app for CodeQL scan. See [#1221](https://github.com/DataDog/dd-sdk-android/pull/1221) +* [MAINTENANCE] Merge release 1.16.0 branch into develop. See [#1228](https://github.com/DataDog/dd-sdk-android/pull/1228) +* [MAINTENANCE] Merge develop into SDK v2 branch. See [#1224](https://github.com/DataDog/dd-sdk-android/pull/1224) +* [MAINTENANCE] Gradle 7.6 & AGP 7.4.0. See [#1232](https://github.com/DataDog/dd-sdk-android/pull/1232) +* [MAINTENANCE] Merge SDK v2 branch into develop branch. See [#1237](https://github.com/DataDog/dd-sdk-android/pull/1237) +* [MAINTENANCE] Use custom detekt rule. See [#1233](https://github.com/DataDog/dd-sdk-android/pull/1233) + +# 1.16.0 / 2023-01-10 + +* [BUGFIX] Global: Use safe context for directBootAware host apps. See [#1209](https://github.com/DataDog/dd-sdk-android/pull/1209) +* [BUGFIX] Global: Provide frozen snapshot of features context when requested. See [#1213](https://github.com/DataDog/dd-sdk-android/pull/1213) +* [IMPROVEMENT] Tracing: Tracing feature stores context in the common context storage. See [#1216](https://github.com/DataDog/dd-sdk-android/pull/1216) +* [IMPROVEMENT] Telemetry: Apply extra sampling rate to the configuration telemetry. See [#1222](https://github.com/DataDog/dd-sdk-android/pull/1222) + +# 1.16.0-beta1 / 2022-12-13 + +* [FEATURE] Global: Unlock encryption API for SDK v2. See [#935](https://github.com/DataDog/dd-sdk-android/pull/935) +* [FEATURE] RUM: Add telemetry configuration mapper. See [#1142](https://github.com/DataDog/dd-sdk-android/pull/1142) +* [FEATURE] Logs: Add a logger method to log error information from strings. See [#1143](https://github.com/DataDog/dd-sdk-android/pull/1143) +* [IMPROVEMENT] Global: Ensure thread safety. See [#936](https://github.com/DataDog/dd-sdk-android/pull/936) +* [IMPROVEMENT] RUM: Use RumFeature#context instead of CoreFeature#contextRef in RumFeature. See [#982](https://github.com/DataDog/dd-sdk-android/pull/982) +* [IMPROVEMENT] Global: Observe uncaught exception in executors. See [#1125](https://github.com/DataDog/dd-sdk-android/pull/1125) +* [IMPROVEMENT] RUM: Update default values for RUM events. See [#1139](https://github.com/DataDog/dd-sdk-android/pull/1139) +* [IMPROVEMENT] Logs: Add device.architecture to logs. See [#1140](https://github.com/DataDog/dd-sdk-android/pull/1140) +* [BUGFIX] Logs: Make a local copy of tags before creating LogEvent. See [#1172](https://github.com/DataDog/dd-sdk-android/pull/1172) +* [BUGFIX] RUM: Synchronize access to DatadogRumMonitor#rootScope when processing fatal error. See [#1186](https://github.com/DataDog/dd-sdk-android/pull/1186) +* [DOCS] Small Link Nit. See [#1028](https://github.com/DataDog/dd-sdk-android/pull/1028) +* [DOCS] Android Data Collected Edits. See [#1059](https://github.com/DataDog/dd-sdk-android/pull/1059) +* [DOCS] Fix sample in README. See [#1141](https://github.com/DataDog/dd-sdk-android/pull/1141) +* [DOCS] Fix sample code in addAction API. See [#1046](https://github.com/DataDog/dd-sdk-android/pull/1046) +* [DOCS] Fix link to setup facets and measures. See [#1179](https://github.com/DataDog/dd-sdk-android/pull/1179) +* [DOCS] Fix typo in CONTRIBUTING.md. See [#1188](https://github.com/DataDog/dd-sdk-android/pull/1188) +* [SDK v2] Datadog singleton. See [#918](https://github.com/DataDog/dd-sdk-android/pull/918) +* [SDK v2] Make SDK Features simple classes. See [#928](https://github.com/DataDog/dd-sdk-android/pull/928) +* [SDK v2] Use TLV format for data storage. See [#931](https://github.com/DataDog/dd-sdk-android/pull/931) +* [SDK v2] Single Storage. See [#932](https://github.com/DataDog/dd-sdk-android/pull/932) +* [SDK v2] Create Feature configuration interfaces. See [#933](https://github.com/DataDog/dd-sdk-android/pull/933) +* [SDK v2] Write batch metadata. See [#943](https://github.com/DataDog/dd-sdk-android/pull/943) +* [SDK v2] Rework file persistence layer. See [#947](https://github.com/DataDog/dd-sdk-android/pull/947) +* [SDK v2] SDK v2 upload pipeline. See [#956](https://github.com/DataDog/dd-sdk-android/pull/956) +* [SDK v2] Data is written in the SDK specific location. See [#975](https://github.com/DataDog/dd-sdk-android/pull/975) +* [SDK v2] Bring tests back. See [#977](https://github.com/DataDog/dd-sdk-android/pull/977) +* [SDK v2] Provide core SDK context. See [#988](https://github.com/DataDog/dd-sdk-android/pull/988) +* [SDK v2] Improvement to the reading batch logic. See [#992](https://github.com/DataDog/dd-sdk-android/pull/992) +* [SDK v2] Features can store their context in SDK context. See [#1036](https://github.com/DataDog/dd-sdk-android/pull/1036) +* [SDK v2] Use SDK v2 components in the upload pipeline. See [#1040](https://github.com/DataDog/dd-sdk-android/pull/1040) +* [SDK v2] Switch to the SDK v2 storage component. See [#1051](https://github.com/DataDog/dd-sdk-android/pull/1051) +* [SDK v2] Update DatadogCore initialization tests. See [#1056](https://github.com/DataDog/dd-sdk-android/pull/1056) +* [SDK v2] Register V1 features as V2. See [#1069](https://github.com/DataDog/dd-sdk-android/pull/1069) +* [SDK v2] Create storage and uploader outside of the feature. See [#1070](https://github.com/DataDog/dd-sdk-android/pull/1070) +* [SDK v2] Remove DataReader v1 usages. See [#1071](https://github.com/DataDog/dd-sdk-android/pull/1071) +* [SDK v2] Use SDK v2 configuration interfaces for features. See [#1079](https://github.com/DataDog/dd-sdk-android/pull/1079) +* [SDK v2] Simple message bus for cross-feature communication. See [#1087](https://github.com/DataDog/dd-sdk-android/pull/1087) +* [SDK v2] Make Storage#writeCurrentBatch async. See [#1094](https://github.com/DataDog/dd-sdk-android/pull/1094) +* [SDK v2] Use event write context for Logs. See [#1103](https://github.com/DataDog/dd-sdk-android/pull/1103) +* [SDK v2] Use event write context for Traces. See [#1106](https://github.com/DataDog/dd-sdk-android/pull/1106) +* [SDK v2] Use event write context for Session Replay. See [#1107](https://github.com/DataDog/dd-sdk-android/pull/1107) +* [SDK v2] Use event write context in RUM. See [#1117](https://github.com/DataDog/dd-sdk-android/pull/1117) +* [SDK v2] Remove RumEventSourceProvider. See [#1119](https://github.com/DataDog/dd-sdk-android/pull/1119) +* [SDK v2] Use event write context in WebView Logs. See [#1121](https://github.com/DataDog/dd-sdk-android/pull/1121) +* [SDK v2] Make implementations of EventBatchWriter return result instead of using listener. See [#1097](https://github.com/DataDog/dd-sdk-android/pull/1097) +* [SDK v2] Avoid capturing shared state by Event Write Context. See [#1126](https://github.com/DataDog/dd-sdk-android/pull/1126) +* [SDK v2] Move global RUM context into generic feature context storage. See [#1146](https://github.com/DataDog/dd-sdk-android/pull/1146) +* [SDK v2] Improve SDK performance a bit. See [#1153](https://github.com/DataDog/dd-sdk-android/pull/1153) +* [SDK v2] Create implementation of InternalLogger. See [#1155](https://github.com/DataDog/dd-sdk-android/pull/1155) +* [SDK v2] Fix devLogger. See [#1156](https://github.com/DataDog/dd-sdk-android/pull/1156) +* [SDK v2] Prepare merge of SDK v2 branch into develop branch. See [#1158](https://github.com/DataDog/dd-sdk-android/pull/1158) +* [SDK v2] Fix flaky TLV format reader test. See [#1166](https://github.com/DataDog/dd-sdk-android/pull/1166) +* [SDK v2] Improve batch upload wait timeout handling. See [#1182](https://github.com/DataDog/dd-sdk-android/pull/1182) +* [SDK v2] Fix possible crash during the telemetry processing. See [#1184](https://github.com/DataDog/dd-sdk-android/pull/1184) +* [SESSION REPLAY] Setup dd-sdk-android-session-replay module. See [#953](https://github.com/DataDog/dd-sdk-android/pull/953) +* [SESSION REPLAY] Add Session Replay Public API. See [#974](https://github.com/DataDog/dd-sdk-android/pull/974) +* [SESSION REPLAY] Add Session Replay Public API tests. See [#985](https://github.com/DataDog/dd-sdk-android/pull/985) +* [SESSION REPLAY] Generate Session Replay modes based on the JSON schemas. See [#986](https://github.com/DataDog/dd-sdk-android/pull/986) +* [SESSION REPLAY] Add Session Replay lifecycle callbacks. See [#993](https://github.com/DataDog/dd-sdk-android/pull/993) +* [SESSION REPLAY] Adapt PokoGenerator and Session Replay models generator to new schemas. See [#1002](https://github.com/DataDog/dd-sdk-android/pull/1002) +* [SESSION REPLAY] Add session replay recorder basic logic. See [#1007](https://github.com/DataDog/dd-sdk-android/pull/1007) +* [SESSION REPLAY] Intercept window touch events as Session Replay TouchData. See [#1009](https://github.com/DataDog/dd-sdk-android/pull/1009) +* [SESSION REPLAY] Processor - process FullSnapshotRecords and dispatch to persister. See [#1013](https://github.com/DataDog/dd-sdk-android/pull/1013) +* [SESSION REPLAY] Process Session Replay touch and orientation change events. See [#1014](https://github.com/DataDog/dd-sdk-android/pull/1014) +* [SESSION REPLAY] SnapshotProcessor - process wireframe mutations. See [#1033](https://github.com/DataDog/dd-sdk-android/pull/1033) +* [SESSION REPLAY] Add the Session Replay MASK_ALL/ALLOW_ALL privacy strategies. See [#1035](https://github.com/DataDog/dd-sdk-android/pull/1035) +* [SESSION REPLAY] Use View hashcode as unique identifier for mapped Wireframe. See [#1037](https://github.com/DataDog/dd-sdk-android/pull/1037) +* [SESSION REPLAY] SnapshotProcessor - do not write anything if there was no mutation detected. See [#1038](https://github.com/DataDog/dd-sdk-android/pull/1038) +* [SESSION REPLAY] Create the Session Replay Writer component. See [#1041](https://github.com/DataDog/dd-sdk-android/pull/1041) +* [SESSION REPLAY] Send hasReplay property for RUM events. See [#1054](https://github.com/DataDog/dd-sdk-android/pull/1054) +* [SESSION REPLAY] Add Session Replay Uploader. See [#1063](https://github.com/DataDog/dd-sdk-android/pull/1063) +* [SESSION REPLAY] Fix the mutation resolver alg based on the Heckels definition. See [#1065](https://github.com/DataDog/dd-sdk-android/pull/1065) +* [SESSION REPLAY] Use RUM timestamp offset when resolving Session Replay timestamp. See [#1068](https://github.com/DataDog/dd-sdk-android/pull/1068) +* [SESSION REPLAY] Correct the way we handle orientation change events in SR. See [#1073](https://github.com/DataDog/dd-sdk-android/pull/1073) +* [SESSION REPLAY] Optimize BaseWireframeMapper#colorAndAlphaAsStringHexa function. See [#1077](https://github.com/DataDog/dd-sdk-android/pull/1077) +* [SESSION REPLAY] Accept optional Typeface when resolving fontStyle. See [#1082](https://github.com/DataDog/dd-sdk-android/pull/1082) +* [SESSION REPLAY] Skip new lines and spaces when obfuscating texts. See [#1078](https://github.com/DataDog/dd-sdk-android/pull/1078) +* [SESSION REPLAY] Correctly handle clipped elements and scrollable lists. See [#1101](https://github.com/DataDog/dd-sdk-android/pull/1101) +* [SESSION REPLAY] Flush buffered motion event positions periodically. See [#1108](https://github.com/DataDog/dd-sdk-android/pull/1108) +* [SESSION REPLAY] Fix source key in SR upload form. See [#1109](https://github.com/DataDog/dd-sdk-android/pull/1109) +* [SESSION REPLAY] Increase the screen snapshot frequency. See [#1110](https://github.com/DataDog/dd-sdk-android/pull/1110) +* [SESSION REPLAY] Start/Stop Session recording based on the RUM session state. See [#1122](https://github.com/DataDog/dd-sdk-android/pull/1122) +* [SESSION REPLAY] Sync SR touch and screen recorders. See [#1150](https://github.com/DataDog/dd-sdk-android/pull/1150) +* [SESSION REPLAY] Correctly resolve ShapeStyle opacity in Wireframes. See [#1152](https://github.com/DataDog/dd-sdk-android/pull/1152) +* [MAINTENANCE] Merge develop into sdkv2 branch. See [#978](https://github.com/DataDog/dd-sdk-android/pull/978) +* [MAINTENANCE] Add support to Json Schema's oneOf syntax. See [#976](https://github.com/DataDog/dd-sdk-android/pull/976) +* [MAINTENANCE] Small fix in the PokoGenerator tool. See [#989](https://github.com/DataDog/dd-sdk-android/pull/989) +* [MAINTENANCE] Use Gradle lazy API more. See [#996](https://github.com/DataDog/dd-sdk-android/pull/996) +* [MAINTENANCE] Gradle 7.5. See [#997](https://github.com/DataDog/dd-sdk-android/pull/997) +* [MAINTENANCE] Merge develop branch into SDK v2 branch. See [#1034](https://github.com/DataDog/dd-sdk-android/pull/1034) +* [MAINTENANCE] Merge 1.14.0 release branch into master branch . See [#1048](https://github.com/DataDog/dd-sdk-android/pull/1048) +* [MAINTENANCE] Merge develop into SDK v2 branch. See [#1052](https://github.com/DataDog/dd-sdk-android/pull/1052) +* [MAINTENANCE] Merge release 1.14.1 into master. See [#1062](https://github.com/DataDog/dd-sdk-android/pull/1062) +* [MAINTENANCE] Merge Session Replay branch into SDK v2 branch. See [#1075](https://github.com/DataDog/dd-sdk-android/pull/1075) +* [MAINTENANCE] Merge develop into SDK v2 branch. See [#1076](https://github.com/DataDog/dd-sdk-android/pull/1076) +* [MAINTENANCE] Apply ktlint 0.45.1 rules. See [#1086](https://github.com/DataDog/dd-sdk-android/pull/1086) +* [MAINTENANCE] Move package from AndroidManifest to namespace plugin config property for Session Replay module. See [#1095](https://github.com/DataDog/dd-sdk-android/pull/1095) +* [MAINTENANCE] Merge develop into SDK v2 branch. See [#1104](https://github.com/DataDog/dd-sdk-android/pull/1104) +* [MAINTENANCE] Fix caching of models generation task. See [#1111](https://github.com/DataDog/dd-sdk-android/pull/1111) +* [MAINTENANCE] Fix WireframeUtils flaky tests. See [#1113](https://github.com/DataDog/dd-sdk-android/pull/1113) +* [MAINTENANCE] Fix flaky tests in WireframeUtilsTest. See [#1128](https://github.com/DataDog/dd-sdk-android/pull/1128) +* [MAINTENANCE] Next dev cycle. See [#1132](https://github.com/DataDog/dd-sdk-android/pull/1132) +* [MAINTENANCE] Suppress the deprecation of Bundle#get in nightly test. See [#1137](https://github.com/DataDog/dd-sdk-android/pull/1137) +* [MAINTENANCE] Remove non-existent function in dogfood script. See [#1136](https://github.com/DataDog/dd-sdk-android/pull/1136) +* [MAINTENANCE] Merge release 1.15.0 into develop. See [#1135](https://github.com/DataDog/dd-sdk-android/pull/1135) +* [MAINTENANCE] Merge release 1.15.0 into master. See [#1134](https://github.com/DataDog/dd-sdk-android/pull/1134) +* [MAINTENANCE] Merge develop branch into SDK v2 branch. See [#1138](https://github.com/DataDog/dd-sdk-android/pull/1138) +* [MAINTENANCE] Suppress the deprecation of Bundle#get in nightly test. See [#1144](https://github.com/DataDog/dd-sdk-android/pull/1144) +* [MAINTENANCE] Update telemetry schema. See [#1145](https://github.com/DataDog/dd-sdk-android/pull/1145) +* [MAINTENANCE] Merge develop into SDK v2 branch. See [#1147](https://github.com/DataDog/dd-sdk-android/pull/1147) +* [MAINTENANCE] Add fromJsonElement method to generated models. See [#1148](https://github.com/DataDog/dd-sdk-android/pull/1148) +* [MAINTENANCE] Fix flakiness in DebouncerTest class. See [#1151](https://github.com/DataDog/dd-sdk-android/pull/1151) +* [MAINTENANCE] Fix unit tests flakiness in MutationResolverTest. See [#1157](https://github.com/DataDog/dd-sdk-android/pull/1157) +* [MAINTENANCE] Merge SDK v2 branch into develop branch. See [#1159](https://github.com/DataDog/dd-sdk-android/pull/1159) +* [MAINTENANCE] Add publish task for the Session Replay module. See [#1162](https://github.com/DataDog/dd-sdk-android/pull/1162) +* [MAINTENANCE] Nightly tests improvements. See [#1163](https://github.com/DataDog/dd-sdk-android/pull/1163) +* [MAINTENANCE] Fix some nightly tests flakiness. See [#1165](https://github.com/DataDog/dd-sdk-android/pull/1165) +* [MAINTENANCE] Update telemetry configuration schema. See [#1175](https://github.com/DataDog/dd-sdk-android/pull/1175) +* [MAINTENANCE] Merge master into develop. See [#1183](https://github.com/DataDog/dd-sdk-android/pull/1183) +* [MAINTENANCE] Apply new detekt configuration. See [#1180](https://github.com/DataDog/dd-sdk-android/pull/1180) +* [MAINTENANCE] Fix flaky BroadcastReceiverSystemInfoProvider test. See [#1185](https://github.com/DataDog/dd-sdk-android/pull/1185) +* [MAINTENANCE] Generated code. See [#1189](https://github.com/DataDog/dd-sdk-android/pull/1189) + +# 1.15.0 / 2022-11-09 + +* [FEATURE] RUM: Add frustration signal 'Error Tap'. See [#1006](https://github.com/DataDog/dd-sdk-android/pull/1006) +* [FEATURE] RUM: Report frustration count on views. See [#1030](https://github.com/DataDog/dd-sdk-android/pull/1030) +* [FEATURE] RUM: Add API to enable/disable tracking of frustration signals. See [#1085](https://github.com/DataDog/dd-sdk-android/pull/1085) +* [FEATURE] RUM: Create internal API for sending technical performance metrics. See [#1083](https://github.com/DataDog/dd-sdk-android/pull/1083) +* [FEATURE] RUM: Configuration Telemetry. See [#1118](https://github.com/DataDog/dd-sdk-android/pull/1118) +* [IMPROVEMENT] Internal: Add internal DNS resolver. See [#991](https://github.com/DataDog/dd-sdk-android/pull/991) +* [IMPROVEMENT] RUM: Support sending CPU architecture as part of device info. See [#1000](https://github.com/DataDog/dd-sdk-android/pull/1000) +* [IMPROVEMENT] Internal: Add checks on intake request headers. See [#1005](https://github.com/DataDog/dd-sdk-android/pull/1005) +* [IMPROVEMENT] RUM: Enable custom application version support. See [#1020](https://github.com/DataDog/dd-sdk-android/pull/1020) +* [IMPROVEMENT] RUM: Add configuration method to disable action tracking. See [#1023](https://github.com/DataDog/dd-sdk-android/pull/1023) +* [IMPROVEMENT] Global: Minor performance optimization during serialization into JSON format. See [#1066](https://github.com/DataDog/dd-sdk-android/pull/1066) +* [IMPROVEMENT] Global: Editable additional attributes. See [#1089](https://github.com/DataDog/dd-sdk-android/pull/1089) +* [IMPROVEMENT] RUM: Add tracing sampling attribute. See [#1092](https://github.com/DataDog/dd-sdk-android/pull/1092) +* [IMPROVEMENT] RUM: Invert frame time to get js refresh rate. See [#1105](https://github.com/DataDog/dd-sdk-android/pull/1105) +* [IMPROVEMENT] Global: Target Android 13. See [#1130](https://github.com/DataDog/dd-sdk-android/pull/1130) +* [BUGFIX] Internal: Fix buttons overlap in the sample app. See [#1004](https://github.com/DataDog/dd-sdk-android/pull/1004) +* [BUGFIX] Global: Prevent crash on peekBody. See [#1080](https://github.com/DataDog/dd-sdk-android/pull/1080) +* [BUGFIX] Shutdown Kronos clock only after executors. See [#1127](https://github.com/DataDog/dd-sdk-android/pull/1127) +* [DOCS] Android and Android TV Monitoring Formatting Edit. See [#966](https://github.com/DataDog/dd-sdk-android/pull/966) +* [DOCS] Update statement about default view tracking strategy in the docs. See [#1003](https://github.com/DataDog/dd-sdk-android/pull/1003) +* [DOCS] Android Monitoring Doc Edit. See [#1010](https://github.com/DataDog/dd-sdk-android/pull/1010) +* [DOCS] Adds Secondary Doc Reviewer. See [#1011](https://github.com/DataDog/dd-sdk-android/pull/1011) +* [DOCS] Replace references to iOS classes with correct Android equivalents. See [#1012](https://github.com/DataDog/dd-sdk-android/pull/1012) +* [DOCS] Add setVitalsUpdateFrequency to the doc. See [#1015](https://github.com/DataDog/dd-sdk-android/pull/1015) +* [DOCS] Android Integrated Libraries Update. See [#1021](https://github.com/DataDog/dd-sdk-android/pull/1021) +* [DOCS] Sync Doc Changes From Master to Develop. See [#1024](https://github.com/DataDog/dd-sdk-android/pull/1024) +* [MAINTENANCE] CI: Use a gitlab template for analysis generated files check. See [#1016](https://github.com/DataDog/dd-sdk-android/pull/1016) +* [MAINTENANCE] Update Cmake to 3.22.1. See [#1032](https://github.com/DataDog/dd-sdk-android/pull/1032) +* [MAINTENANCE] Update CODEOWNERS. See [#1045](https://github.com/DataDog/dd-sdk-android/pull/1045) +* [MAINTENANCE] Update AGP to 7.3.0. See [#1060](https://github.com/DataDog/dd-sdk-android/pull/1060) +* [MAINTENANCE] CI: Use KtLint 0.45.1 and dedicated runner image. See [#1081](https://github.com/DataDog/dd-sdk-android/pull/1081) +* [MAINTENANCE] CI: Migrate ktlint CI job to shared gitlab template. See [#1084](https://github.com/DataDog/dd-sdk-android/pull/1084) +* [MAINTENANCE] Update ktlint to 0.47.1. See [#1091](https://github.com/DataDog/dd-sdk-android/pull/1091) +* [MAINTENANCE] Publish SNAPSHOT builds to sonatype on pushes to develop. See [#1093](https://github.com/DataDog/dd-sdk-android/pull/1093) +* [MAINTENANCE] Add version to top level project for nexusPublishing extension. See [#1096](https://github.com/DataDog/dd-sdk-android/pull/1096) +* [MAINTENANCE] Fix import ordering. See [#1098](https://github.com/DataDog/dd-sdk-android/pull/1098) +* [MAINTENANCE] Deprecate DatadogPlugin class and its usage. See [#1100](https://github.com/DataDog/dd-sdk-android/pull/1100) +* [MAINTENANCE] CI: Update .gitlab-ci.yml to use release image for static-analysis job. See [#1102](https://github.com/DataDog/dd-sdk-android/pull/1102) +* [MAINTENANCE] Suppress DatadogPlugin deprecation for instrumented tests. See [#1114](https://github.com/DataDog/dd-sdk-android/pull/1114) +* [MAINTENANCE] Remove Flutter from Dogfooding scripts. See [#1120](https://github.com/DataDog/dd-sdk-android/pull/1120) +* [MAINTENANCE] Fix flaky ANR detection test. See [#1123](https://github.com/DataDog/dd-sdk-android/pull/1123) +* [MAINTENANCE] Android Gradle Plugin 7.3.1. See [#1124](https://github.com/DataDog/dd-sdk-android/pull/1124) +* [MAINTENANCE] Filter out telemetry in the assertions of instrumented RUM tests. See [#1131](https://github.com/DataDog/dd-sdk-android/pull/1131) + +# 1.14.1 / 2022-09-27 + +* [IMPROVEMENT] Global: Add CPU architecture to the collected device information. See [#1000](https://github.com/DataDog/dd-sdk-android/pull/1000) + +# 1.14.0 / 2022-09-20 + +* [FEATURE] Global: Collect OS and device information instead of relying on User-Agent header. See [#945](https://github.com/DataDog/dd-sdk-android/pull/945) +* [IMPROVEMENT] Logs: Add a possibility to define min log level. See [#920](https://github.com/DataDog/dd-sdk-android/pull/920) +* [IMPROVEMENT] Logs: Add `variant` tag to events. See [#1025](https://github.com/DataDog/dd-sdk-android/pull/1025) +* [IMPROVEMENT] RUM: Add a method to add extra user properties. See [#952](https://github.com/DataDog/dd-sdk-android/pull/952) (Thanks [@JosephRoskopf](https://github.com/JosephRoskopf)) +* [IMPROVEMENT] RUM: Allow to configure Vitals collection frequency. See [#926](https://github.com/DataDog/dd-sdk-android/pull/926) +* [IMPROVEMENT] RUM: Improve session management logic. See [#948](https://github.com/DataDog/dd-sdk-android/pull/948) +* [IMPROVEMENT] RUM: Back navigation is reported with `back` type. See [#980](https://github.com/DataDog/dd-sdk-android/pull/980) +* [IMPROVEMENT] RUM: Add a possibility to disable automatic view tracking. See [#981](https://github.com/DataDog/dd-sdk-android/pull/981) +* [IMPROVEMENT] RUM: Add a possibility to disable automatic interactions tracking. See [#1023](https://github.com/DataDog/dd-sdk-android/pull/1023) +* [IMPROVEMENT] Global: Remove deprecated APIs. See [#973](https://github.com/DataDog/dd-sdk-android/pull/973) + +# 1.13.0 / 2022-06-27 + +* [BUGFIX] Core: Prevent a rare race condition in the features folder creation +* [BUGFIX] RUM: Update Global RUM context when view is stopped +* [BUGFIX] RUM: Interactions use the window coordinates, and not the screen ones +* [FEATURE] RUM: Add compatibility with Android TV application (see our dedicated artifact to track TV actions) +* [FEATURE] Global: Provide an internal observability mechanism +* [IMPROVEMENT] Global: improve the local LogCat messages from the SDK +* [IMPROVEMENT] RUM: allow client side sampling for RUM Resource tracing +* [IMPROVEMENT] RUM: Disable Vitals collection when app's in background +* [IMPROVEMENT] RUM: Reduce the size of events sent + +# 1.12.0 / 2022-04-11 + +* [BUGFIX] Core: Internal attributes coming from cross-platform are removed after being read +* [BUGFIX] RUM: Ensure Crash report works even when there're no active view [#849](https://github.com/DataDog/dd-sdk-android/issues/849) (Thanks [@emichaux](https://github.com/emichaux)) +* [BUGFIX] RUM: Span created from network requests stop sending the query params in the `resource` attribute +* [BUGFIX] RUM: Ongoing action completes when a new view starts +* [FEATURE] RUM: Allow tracking browser RUM events from Webviews +* [FEATURE] RUM: Add support to Jetpack Compose +* [IMPROVEMENT] Logs: Support adding org.json.JSONObject attributes to Loggers [#588](https://github.com/DataDog/dd-sdk-android/issues/588) (Thanks [@fleficher](https://github.com/fleficher)) +* [IMPROVEMENT] RUM: Automatically track Activity Intents +* [IMPROVEMENT] RUM: Collect RUM events during application launch +* [IMPROVEMENT] RUM: Remove RUM Action automatic filtering +* [IMPROVEMENT] Global: Prevent 3rd party/system dependency calls from crashing the host application +* [IMPROVEMENT] Global: Use the cache folder to store batch files + +# 1.11.1 / 2022-01-06 + +* [BUGFIX] RUM: Prevent potential crash when targeting Android SDK 31 [#709](https://github.com/DataDog/dd-sdk-android/issues/709) (Thanks [@mattking-chip](https://github.com/mattking-chip)) + +# 1.11.0 / 2021-12-07 + +* [BUGFIX] RUM: Fix Memory Vital downscaled on Android +* [BUGFIX] RUM: Prevent potential crash in DatadogExceptionHandler [#749](https://github.com/DataDog/dd-sdk-android/issues/749) (Thanks [@ribafish](https://github.com/ribafish)) +* [BUGFIX] RUM: Prevent potential crash in WindowCallbackWrapper +* [BUGFIX] RUM: Prevent potential crash NdkCrashReportsPlugin +* [BUGFIX] RUM: Ensure all crash information are saved +* [BUGFIX] Global: Prevent crash on init on KitKat devices [#678](https://github.com/DataDog/dd-sdk-android/issues/678) (Thanks [@eduardb](https://github.com/eduardb)) +* [FEATURE] RUM: Add new Mobile Vitals attributes +* [FEATURE] RUM: Adds an optional `RumSessionListener` in the `RumMonitor.Builder` to know when a session starts, and what its UUID is +* [FEATURE] Global: Allow user to set a custom proxy for data intake [#592](https://github.com/DataDog/dd-sdk-android/issues/592) (Thanks [@ruXlab](https://github.com/ruXlab)) +* [IMPROVEMENT] RUM: Associate Logs and Traces with RUM Action +* [IMPROVEMENT] RUM: Tag RUM Resources as `native` by default (instead of `xhr`) +* [IMPROVEMENT] RUM: Sanitize NDK crash stacktraces +* [IMPROVEMENT] RUM: Enrich RUM Errors with the Throwable's message +* [IMPROVEMENT] Global: Update the intake request for Datadog's API v2 +* [IMPROVEMENT] Global: Add support to US5 endpoint +* [IMPROVEMENT] RUM: Prevent Get leaking memory with OkHttp ResponseBody objects +* [IMPROVEMENT] RUM: Add an action predicate to rename the action target + +# 1.10.0 / 2021-09-02 + +* [BUGFIX] Global: Fix crash when using old OkHttp dependency [#658](https://github.com/DataDog/dd-sdk-android/issues/658) (Thanks [@JessicaYeh](https://github.com/VladBytsyuk)) +* [BUGFIX] Global: Prevent retrying endlessly data upload when Client Token is invalid +* [BUGFIX] Global: Support using DD Android SDK with Java 11 +* [BUGFIX] Global: Support proper serialization of nested maps for custom attributes +* [BUGFIX] RUM: Ensure all crashes are reported to RUM +* [FEATURE] APM: Add Data Scrubbing for Spans +* [FEATURE] RUM: Detect ANR +* [FEATURE] RUM: Track Memory and CPU usage, as well as views' refresh rate +* [FEATURE] RUM: Track Actions, Resources and Errors when the application is in background (see `Configuration.Builder().trackBackgroundRumEvents(true).build()`) +* [FEATURE] RUM: Add data scrubbing for Long Tasks +* [IMPROVEMENT] RUM: Replace all `other` or `unknown` resources with xhr (ensuring end-to-end trace is enabled) +* [IMPROVEMENT] RUM: Let children events have the proper view.name attribute +* [IMPROVEMENT] RUM: Keep all custom Action even when a previous action is still active +* [IMPROVEMENT] Global: Update available endpoints (and match documentation names: `US1`, `US3`, `US1_FED` and `EU1`) +* [IMPROVEMENT] Global: All user info should be in usr.* + +# 1.9.0 / 2021-06-07 + +* [BUGFIX] APM: Fix network tracing inconsistencies +* [BUGFIX] APM: Fix span with custom `MESSAGE` field [#522](https://github.com/DataDog/dd-sdk-android/issues/522) (Thanks [@JessicaYeh](https://github.com/JessicaYeh)) +* [BUGFIX] Logs: Fix tag name in Timber `DatadogTree` [#483](https://github.com/DataDog/dd-sdk-android/issues/483) (Thanks [@cfa-eric](https://github.com/cfa-eric)) +* [BUGFIX] RUM: Ensure View linked events count is correct when events are discarded +* [BUGFIX] RUM: Fix Resource network timings +* [BUGFIX] APM: Fix span logs timestamp conversion +* [FEATURE] RUM: Detect Long Tasks (tasks blocking the main thread) +* [FEATURE] RUM: add a callback to enrich RUM Resources created from OkHttp Requests +* [IMPROVEMENT] RUM: Remove the "Application crash detected" prefix and ensure the message is kept +* [IMPROVEMENT] RUM: Add warning when a RUM Action is dropped + +# 1.8.1 / 2021-03-04 + +* [BUGFIX] RUM/APM: handle correctly known hosts in global configuration and interceptors [#513](https://github.com/DataDog/dd-sdk-android/issues/513) (Thanks [@erawhctim](https://github.com/erawhctim)) + +# 1.8.0 / 2021-02-25 + +* [BUGFIX] Global: handle correctly incorrect domain names in Interceptors' known hosts +* [BUGFIX] RUM: RUM Context was bundled in spans even when RUM was not enabled +* [FEATURE] Global: Allow user to configure the Upload Frequency (see `Configuration.Builder().setUploadFrequency(…).build()`) +* [FEATURE] Global: Allow user to configure the Batch Size (see `Configuration.Builder().setBatchSize(…).build()`) +* [FEATURE] RUM: Customize Views' name +* [FEATURE] RUM: Send NDK Crash related RUM Error +* [FEATURE] RUM: Track custom timings in RUM Views (see `GlobalRum.get().addTiming("")`) +* [FEATURE] RUM: Provide a PII Data Scrubbing feature (see `Configuration.Builder().setRum***EventMapper(…).build()`) +* [FEATURE] RUM: Send NDK Crash related RUM Error +* [IMPROVEMENT] APM: Stop duplicating APM errors as RUM errors +* [IMPROVEMENT] Logs Align the 'error.kind' attribute value with RUM Error 'error.type' +* [IMPROVEMENT] RUM: Get a more accurate Application loading time +* [IMPROVEMENT] RUM: Add a variant tag on RUM events + +# 1.7.0 / 2021-01-04 + +* [BUGFIX] RUM: fix RUM Error timestamps +* [BUGFIX] RUM: calling `GlobalRum.addAttribute()` with a `null` value would make the application crash +* [BUGFIX] RUM: Actions created with type Custom where sometimes dropped +* [FEATURE] Global: Add support for GDPR compliance feature (see `Datadog.setTrackingConsent()`) +* [FEATURE] Global: Allow setting custom user specific attributes (see `Datadog.setUserInfo()`) +* [IMPROVEMENT] Crash Report: Handle SIGABRT signal in the NDKCrashReporter +* [OTHER] Global: Remove deprecated APIs and warn about future deprecations +* [OTHER] Global: Remove all flavors from sample (allowing to get faster build times) + # 1.6.1 / 2020-11-13 * [BUGFIX] Global: Ensure the network status is properly retrieved on startup @@ -8,9 +1481,8 @@ * [BUGFIX] RUM: Resources are linked with the wrong Action * [BUGFIX] Global: Validate the env value passed in the DatadogConfig.Builder * [BUGFIX] RUM: prevent `trackInterations()` from messing with the Application's theme -* [BUGFIX] Global: Remove unnecessary transitive dependencies from library [#396](https://github.com/DataDog/dd-sdk-android/issues/396) (Thanks @rashadsookram) +* [BUGFIX] Global: Remove unnecessary transitive dependencies from library [#396](https://github.com/DataDog/dd-sdk-android/issues/396) (Thanks [@rashadsookram](https://github.com/rashadsookram)) * [BUGFIX] Global: Prevent a crash in CallbackNetworkProvider - * [FEATURE] Global: Provide an RxJava integration (`dd-sdk-android-rx`) * [FEATURE] Global: Provide a Coil integration (`dd-sdk-android-coil`) * [FEATURE] Global: Provide a Fresco integration (`dd-sdk-android-coil`) @@ -26,7 +1498,7 @@ # 1.5.2 / 2020-09-18 -* [BUGFIX] Global: Prevent a crash when sending data. See [#377](https://github.com/DataDog/dd-sdk-android/issues/377) (Thanks @ronak-earnin) +* [BUGFIX] Global: Prevent a crash when sending data. See [#377](https://github.com/DataDog/dd-sdk-android/issues/377) (Thanks [@ronak-earnin](https://github.com/ronak-earnin)) # 1.5.1 / 2020-09-03 @@ -45,8 +1517,8 @@ * [FEATURE] RUM: Add helper library to track Glide requests and errors * [FEATURE] CrashReport: Add a helper library to detect C/C++ crashes in Android NDK * [FEATURE] Global: add a method to clear all local unsent data -* [BUGFIX] Trace: Fix clock skew issue in traced requests -* [BUGFIX] Logger: Prevent Logcat noise from our SDK when running Robolectric tests +* [BUGFIX] APM: Fix clock skew issue in traced requests +* [BUGFIX] Logs: Prevent Logcat noise from our SDK when running Robolectric tests * [IMPROVEMENT] Global: Enhance the SDK performance and ensure it works properly in a multi-process application * [OTHER] Global: The DatadogConfig needs a valid environment name (`envName`), applied to all features * [OTHER] Global: The serviceName by default will use your application's package name @@ -68,20 +1540,19 @@ * [FEATURE] Global: Update the SDK initialization code * [FEATURE] Global: Add a Kotlin extension module with Kotlin specific integrations -* [FEATURE] Trace: Implement OpenTracing specifications -* [FEATURE] Trace: Add helper methods to attach an error to a span -* [FEATURE] Trace: Add helper Interceptor to trace OkHttp requests +* [FEATURE] APM: Implement OpenTracing specifications +* [FEATURE] APM: Add helper methods to attach an error to a span +* [FEATURE] APM: Add helper Interceptor to trace OkHttp requests * [FEATURE] Logs: Add sampling option in the Logger * [IMPROVEMENT] Logs: Make the log operations thread safe * [BUGFIX] Logs: Fix rare crash on upload requests -* [BUGFIX] Global: Prevent OutOfMemory crash on upload. See [#164](https://github.com/DataDog/dd-sdk-android/issues/164) (Thanks @alparp27) - +* [BUGFIX] Global: Prevent OutOfMemory crash on upload. See [#164](https://github.com/DataDog/dd-sdk-android/issues/164) (Thanks [@alparp27](https://github.com/alparp27)) # 1.3.1 / 2020-04-30 ### Changes -* [BUGFIX] Fix ConcurrentModificationException crash in the FileReader class. See [#234](https://github.com/DataDog/dd-sdk-android/issues/234) (Thanks @alparp27) +* [BUGFIX] Fix ConcurrentModificationException crash in the FileReader class. See [#234](https://github.com/DataDog/dd-sdk-android/issues/234) (Thanks [@alparp27](https://github.com/alparp27)) # 1.3.0 / 2020-03-02 @@ -99,20 +1570,20 @@ ### Changes -* [BUGFIX] Fix invalid dependency group in `dd-sdk-android-timber`. See [#147](https://github.com/DataDog/dd-sdk-android/issues/147) (Thanks @mduong, @alparp27, @rafaela-stockx) +* [BUGFIX] Fix invalid dependency group in `dd-sdk-android-timber`. See [#147](https://github.com/DataDog/dd-sdk-android/issues/147) (Thanks [@mduong](https://github.com/mduong), [@alparp27](https://github.com/alparp27), [@rafaela-stockx](https://github.com/rafaela-stockx)) # 1.2.1 / 2020-02-19 ### Changes -* [BUGFIX] Fix invalid dependency version in `dd-sdk-android-timber`. See [#138](https://github.com/DataDog/dd-sdk-android/issues/138) (Thanks @mduong) +* [BUGFIX] Fix invalid dependency version in `dd-sdk-android-timber`. See [#138](https://github.com/DataDog/dd-sdk-android/issues/138) (Thanks [@mduong](https://github.com/mduong)) # 1.2.0 / 2020-01-20 ### Changes -* [BUGFIX] Fail silently when trying to initialize the SDK twice. See #86 (Thanks @Vavassor) -* [BUGFIX] Publish the Timber artifact automatically. See #90 (Thanks @Macarse) +* [BUGFIX] Fail silently when trying to initialize the SDK twice. See #86 (Thanks [@Vavassor](https://github.com/Vavassor)) +* [BUGFIX] Publish the Timber artifact automatically. See #90 (Thanks [@Macarse](https://github.com/Macarse)) * [FEATURE] Create a Crash Handler : App crashes will be automatically logged. * [FEATURE] Downgrade OkHttp4 to OkHttp3 * [FEATURE] Make Library compatible with API 19+ @@ -135,7 +1606,7 @@ * [BUGFIX] Make the packageVersion field optional in the SDK initialisation * [BUGFIX] Fix timestamp formatting in logs -* [FEATURE] Add a developer targeted logger +* [FEATURE] Add a developer targeted logger * [FEATURE] Add user info in logs * [FEATURE] Create automatic Tags / Attribute (app / sdk version) * [FEATURE] Integrate SDK with Timber diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1084816a66..b3c9e8ae99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,58 +5,103 @@ 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. + ## Setup your developer Environment -To setup your enviroment, make sure you installed [Android Studio](https://developer.android.com/studio). +To setup your environment, make sure you installed [Android Studio](https://developer.android.com/studio). **Note**: you can also compile and develop using only the Android SDK and your IDE of choice, e.g.: IntelliJ Idea, Vim, etc. +In addition, to be able to run the static analysis tools locally, you should run the `local-ci.sh` script locally as follow. + +```shell +./local_ci.sh --setup +``` + ### Modules This project hosts the following modules: - - `dd-sdk-android`: the main library implementing all Datadog features (Logs, Traces, RUM, Crash reports); - - `dd-sdk-android-ktx`: a set of Kotlin extensions to make the `dd-sdk-android` library more Kotlin friendly; - - `dd-sdk-android-ndk`: a Plugin to allow tracking NDK information; - - `dd-sdk-android-glide`: a lightweight library providing a bridge integration between `dd-sdk-android` and [Glide](https://bumptech.github.io/glide/); - - `dd-sdk-android-timber`: a lightweight library providing a bridge integration between `dd-sdk-android` and [Timber](https://github.com/JakeWharton/timber); - - `instrumented/benchmark`: a test module to verify the performance of the library; - - `instrumented/integration`: a test module with integration tests using Espresso; - - `tools/detekt`: a few custom [Detekt](https://github.com/arturbosch/detekt) static analysis rules; - - `tools/noopfactory`: an annotation processor generating no-op implementation of interfaces; - - `tools/unit`: a utility library with code to help writing unit tests; - - `sample/***`: a few sample application showcasing how to use the library features in production code; + - `dd-sdk-android-core`: the main library implementing the core functionality of SDK (storage and upload of data, core APIs); + - `dd-sdk-android-internal`: a library providing internal APIs, classes and utilities shared by the SDK modules; + - `features/***`: a set of libraries implementing Datadog products: + - `features/dd-sdk-android-logs`: a library to send logs to Datadog; + - `features/dd-sdk-android-rum`: a library to track user navigation and interaction; + - `features/dd-sdk-android-ndk`: a lightweight library to track crashes from NDK libraries; + - `features/dd-sdk-android-session-replay`: a library to capture the window content; + - `features/dd-sdk-android-session-replay-compose`: an extension for Session Replay to integrate with the Jetpack Compose; + - `features/dd-sdk-android-session-replay-material`: an extension for Session Replay to integrate with the Material Design library; + - `features/dd-sdk-android-trace`: a library to measure performance of operations locally; + - `features/dd-sdk-android-trace-api`: a library containing API definitions for the Trace library; + - `features/dd-sdk-android-trace-internal`: a library containing internal classes for the Trace library; + - `features/dd-sdk-android-trace-otel`: an extension of Trace library to integrate with [OpenTelemetry](https://opentelemetry.io/); + - `features/dd-sdk-android-webview`: a library to forward logs and RUM events captured in a webview to be linked with the mobile session; + - `integrations/***`: a set of libraries integrating Datadog products in third party libraries: + - `integrations/dd-sdk-android-apollo`: a lightweight library providing a bridge integration between Datadog SDK and [Apollo Kotlin](https://github.com/apollographql/apollo-kotlin) + - `integrations/dd-sdk-android-coil`: a lightweight library providing a bridge integration between Datadog SDK and [Coil](https://coil-kt.github.io/coil/); + - `integrations/dd-sdk-android-compose`: a lightweight library providing a bridge integration between Datadog SDK and [Jetpack Compose](https://developer.android.com/jetpack/compose); + - `integrations/dd-sdk-android-fresco`: a lightweight library providing a bridge integration between Datadog SDK and [Fresco](https://frescolib.org/); + - `integrations/dd-sdk-android-glide`: a lightweight library providing a bridge integration between Datadog SDK and [Glide](https://bumptech.github.io/glide/); + - `integrations/dd-sdk-android-okhttp`: a lightweight library providing an instrumentation for [OkHttp](https://square.github.io/okhttp/); + - `integrations/dd-sdk-android-okhttp-otel`: a lightweight library providing a support of [OpenTelemetry](https://opentelemetry.io/) for the [OkHttp](https://square.github.io/okhttp/) instrumentation; + - `integrations/dd-sdk-android-rum-coroutines`: a set of extensions for Kotlin Coroutines to ease the work with the RUM library; + - `integrations/dd-sdk-android-rx`: a lightweight library providing a bridge integration between Datadog SDK and [RxJava](https://github.com/ReactiveX/RxJava); + - `integrations/dd-sdk-android-sqldelight`: a lightweight library providing a bridge integration between Datadog SDK and [SQLDelight](https://cashapp.github.io/sqldelight/); + - `integrations/dd-sdk-android-timber`: a lightweight library providing a bridge integration between Datadog SDK and [Timber](https://github.com/JakeWharton/timber); + - `integrations/dd-sdk-android-tv`: a lightweight library providing extensions for [Android TV](https://www.android.com/tv/) + - `integrations/dd-sdk-android-trace-coroutines`: a set of extensions for Kotlin Coroutines to ease the work with the Trace library; + - `instrumented/***`: a set of modules used to run instrumented tests: + - `instrumented/integration`: a test module with integration tests using Espresso; + - `reliability/***`: a set of modules used to run integration tests: + - `reliability/core-it`: a set of integration tests for the Datadog SDK core library; + - `reliability/single-fit/logs`: a set of integration tests for the Logs library; + - `reliability/single-fit/okhttp`: a set of integration tests for [OkHttp](https://square.github.io/okhttp/) instrumentation; + - `reliability/single-fit/rum`: a set of integration tests for the RUM library; + - `reliability/single-fit/trace`: a set of integration tests for the Trace library; + - `reliability/stub-core`: a set of stubs for Datadog SDK core; + - `reliability/stub-feature`: a set of stubs for Datadog SDK feature APIs; + - `tools/***`: a set of modules used to extend the tools we use in our workflow: + - `tools/benchmark`: a code to benchmark SDK performance; + - `tools/detekt`: a few custom [Detekt](https://github.com/arturbosch/detekt) static analysis rules; + - `tools/lint`: a custom [Lint](https://developer.android.com/studio/write/lint) static analysis rule; + - `tools/noopfactory`: an annotation processor generating no-op implementation of interfaces; + - `tools/unit`: a utility library with code to help writing unit tests; + - `sample/***`: a few sample applications showcasing how to use the library features in production code; + - `sample/kotlin`: a sample mobile application; + - `sample/vendor-lib`: a sample Android library, to showcase vendors using Datadog in a host app also using Datadog; + - `sample/wear`: a sample Wear OS application; + - `sample/automotive`: a sample Automotive OS application; + - `sample/tv`: a sample Android TV OS application; + - `sample/benchmark`: a sample application to collect SDK performance metrics; ### Building the SDK You can build the SDK using the following Gradle command: ```shell script -./gradlew assembleAll +./gradlew assembleLibraries ``` ### Running the tests -The whole project is covered by a set of static analysis tools, linters and tests, each triggered by a custom global Gradle task, as follows: +The whole project is covered by a set of static analysis tools, linters and tests. To mimic the steps taken by our CI, you can run the `local_ci.sh` script: ```shell script -# launches the debug and release unit tests for all modules -./gradlew unitTestAll - -# launches the instrumented tests for all modules -./gradlew instrumentTestAll +# cleans the repo +./local_ci.sh --clean -# launches the detekt static analysis for all modules -./gradlew detektAll +# runs the static analysis +./local_ci.sh --analysis -# launches the ktlint format check for all modules -./gradlew ktlintCheckAll +# compiles all the different library modules and tools +./local_ci.sh --compile -# launches the Android linter for all modules -./gradlew lintCheckAll +# Runs the unit tests +./local_ci.sh --test -# launches all the tests described above -./gradlew checkAll +# Update session replay payloads +./local_ci.sh --update-session-replay-payloads ``` ## Submitting Issues @@ -64,7 +109,7 @@ The whole project is covered by a set of static analysis tools, linters and test Many great ideas for new features come from the community, and we'd be happy to consider yours! -To share your request, you can open an [issue](https://github.com/DataDog/dd-sdk-android/issues/new) +To share your request, you can open an [issue](https://github.com/DataDog/dd-sdk-android/issues/new?labels=enhancement&template=FeatureRequest.yml) with the details about what you'd like to see. At a minimum, please provide: - The goal of the new feature; @@ -79,7 +124,7 @@ or UI, contact our support team via https://docs.datadoghq.com/help/ for direct, faster assistance. You may submit bug reports concerning the Datadog SDK for Android by -[opening a Github issue](https://github.com/DataDog/dd-sdk-android/issues/new). +[opening a Github issue](https://github.com/DataDog/dd-sdk-android/issues/new?labels=bug&template=BugReport.yml). At a minimum, please provide: - A description of the problem; @@ -107,7 +152,7 @@ the bug are best. ## Have a patch? We welcome code contributions to the library, which you can -[submit as a pull request](https://github.com/DataDog/dd-sdk-android/pull/new/master). +[submit as a pull request](https://github.com/DataDog/dd-sdk-android/pull/new/develop). Before you submit a PR, make sure that you first create an Issue to explain the bug or the feature your patch covers, and make sure another Issue or PR doesn't already exist. @@ -117,10 +162,8 @@ To create a pull request: 1. **Fork the repository** from https://github.com/DataDog/dd-sdk-android ; 2. **Make any changes** for your patch; 3. **Write tests** that demonstrate how the feature works or how the bug is fixed; -4. **Update any documentation** such as `docs/GettingStarted.md`, especially for - new features; -5. **Submit the pull request** from your fork back to this - [repository](https://github.com/DataDog/dd-sdk-android) . +4. **Update any documentation**, especially for new features. It can be found either in the `docs` folder of this repository, or in [documentation repository](https://github.com/DataDog/documentation); +5. **Submit the pull request** from your fork back to this [repository](https://github.com/DataDog/dd-sdk-android). The pull request will be run through our CI pipeline, and a project member will @@ -144,11 +187,19 @@ any change you introduce are still compatible with Java. If you want to add Kotlin specific features (DSL, lambdas, …), make sure there is a way to get the same feature from a Java source code. +### Code quality + +Our code uses [Detekt](https://detekt.dev/) static analysis with a shared configuration, slightly +stricter than the default one. A Detekt check is ran on every on every PR to ensure that all new code +follow this rule. +Current Detekt version: 1.23.4 + ### Code style Our coding style is ensured by [KtLint](https://ktlint.github.io/), with the default settings. A KtLint check is ran on every PR to ensure that all new code follow this rule. +Current KtLint version: 0.50.0 Classes should group their methods in folding regions named after the declaring class. Private methods should be grouped in an `Internal` named folding region. @@ -157,7 +208,13 @@ following regions. ```kotlin -class Foo :Observable(), Runnable { +class Foo : Observable(), Runnable { + + // region Foo + + fun fooSpecificMethod(){} + + // endregion // region Observable @@ -182,13 +239,12 @@ class Foo :Observable(), Runnable { } ``` -There is also a gradle task that you can use to automatically format the code following the -required styling rules: -```console - -./gradlew :dd-sdk-android:ktlintFormat +There is also a command that you can use to automatically format the code following the +required styling rules (require `ktlint` installed on your machine): +```console +ktlint -F "**/*.kt" "**/*.kts" '!**/build/generated/**' '!**/build/kspCaches/**' ``` ### #TestMatters @@ -207,7 +263,7 @@ We use a variety of tools to help us write tests easy to read and maintain: assertions; - [Elmyr](https://github.com/xgouchet/Elmyr): a framework to generate fake data in the Unit Tests. - + ### Test Conventions In order to make the test classes more readable, here are a set of naming conventions and coding style. @@ -217,17 +273,33 @@ In order to make the test classes more readable, here are a set of naming conven The accepted convention is to use the name of the class under test, with the suffix Test. E.g.: the test class corresponding to the class `Foo` must be named `FooTest`. -### Fields +Some classes need to be created in the `test` sourceSets to integrate with our testing tools +(AssertJ, Elmyr, …). Those classes must be placed in a package named +`{module_package}.tests.{test_library}`, and be named by combining the base class name and +the new class purpose. + +E.g.: + - A custom assertion class for class `Foo` in module `com.datadog.module` will be + `com.datadog.module.tests.assertj.FooAssert` +- A custom forgery factory class for class `Foo` in module `com.datadog.module` will be + `com.datadog.module.tests.elmyr.FooForgeryFactory` + +#### Fields & Test Method parameters Fields should appear in the following order, and be named as explained by these rules: -- The object(s) under test must be named from their class, and prefixed by `tested`. E.g.: `testedListener: Listener`, `testedVisitor: KotlinFileVisitor`. -- Mocked objects must be named from their class (with an optional qualifier), and prefixed by `mock`. E.g.: `mockListener: Listener`, `mockService: ExecutorService`). -- Fake data must be named from their class (with an optional qualifier), and prefixed by `fake`. E.g.: `fakeContext: Context`, `fakeApplicationId: UUID`, `fakeRequest: NetworkRequest`. +- The object(s) under test must be named from their class, and prefixed by `tested`. + E.g.: `testedListener: Listener`, `testedHandler: Handler`. +- Stubbed objects (mocks with predefined behavior) must be named from their class (with an optional qualifier), and prefixed by `stub`. + E.g.: `stubDataProvider: DataProvider`, `stubReader: Reader`. +- Mocked objects (mocks being verified) must be named from their class (with an optional qualifier), and prefixed by `mock`. + E.g.: `mockListener: Listener`, `mockLogger: Logger`. +- Fixtures (data classes or primitives with no behavior) must be named from their class (with an optional qualifier), and prefixed by `fake`. + E.g.: `fakeContext: Context`, `fakeApplicationId: UUID`, `fakeRequest: NetworkRequest`. - Other fields can be named on case by case basis, but a few rules can still apply: - If the field is annotated by a JUnit 5 extension (e.g.: `@TempDir`), then it should be named after the extension (e.g.: `tempOutputDir`). -### Test Methods +#### Test Methods Test methods must follow the Given-When-Then principle, that is they must all consist of three steps: @@ -235,22 +307,29 @@ Test methods must follow the Given-When-Then principle, that is they must all co - When (optional): performs an action — directly or indirectly — on the instance under test; - Then (mandatory): performs any number of assertions on the instance under test’s state, the mocks or output values. It must perform at least one assertion. -If present, these steps will always be intruded by one line comments, e.g.: `// Given`. +If present, these steps will always be intruded by one line comments, i.e.: `// Given`, `// When`, `// Then`. -Based on this principle, the test name should reflect the intent, and use the following pattern: `MUST expected behavior WHEN method() withContext`. To avoid being too verbose, `MUST` will be written `𝕄`, and `WHEN` will be written `𝕎`. The `withContext` part should be concise, and can have a trailing curly braces context section to avoid duplicate names (e.g.: `𝕄 create a span with info 𝕎 intercept() for failing request {5xx}`) +Based on this principle, the test name should reflect the intent, and use the following pattern: `MUST expected behavior WHEN method() GIVEN context`. +To avoid being too verbose, `MUST` will be written `M`, and `WHEN` will be written `W`. The `context` part should be concise, and wrapped in curly braces to avoid duplicate names +(e.g.: `M create a span with info W intercept() {statusCode=5xx}`) -Parameters shall have simple local names reflecting their intent, whether they use an `@Forgery` or `@Mock` annotation (or none). +Parameters shall have simple local names reflecting their intent (see above), whether they use an `@Forgery` or `@Mock` annotation (or none). Here's a test method following those conventions: ```kotlin @Test - fun `𝕄 forward boolean attribute to handler 𝕎 addAttribute()`( - @StringForgery(StringForgeryType.ALPHABETICAL) key : String, - @BoolForgery value : Boolean + fun `M forward boolean attribute to handler W addAttribute()`( + @StringForgery(StringForgeryType.ALPHABETICAL) fakeMessage : String, + @StringForgery(StringForgeryType.ALPHABETICAL) fakeKey : String, + @BoolForgery value : Boolean, + @Mock mockLogHandler: InternalLogger ) { + // Given + testedLogger = Logger(mockLogHandler) + // When - testedLogger.addAttribute(key, value) + testedLogger.addAttribute(fakeKey, value) testedLogger.v(fakeMessage) // Then @@ -265,16 +344,36 @@ Here's a test method following those conventions: } ``` -### Test Utility Methods +#### Test Utility Methods -Because we sometimes need to reuse some setup or assertions in our tests, we tend to write utility methods. Those methods should be private (or internal in a dedicated class/file if they need to be shared across tests). +Because we sometimes need to reuse some setup or assertions in our tests, we tend to write utility methods. +Those methods should be private (or internal in a dedicated class/file if they need to be shared across tests). -- `fun mockSomething([args]): T`: methods setting up a mock. These methods must return the mocked instance; - `fun stubSomething(mock, [args])`: methods setting up a mock (or rarely a fake). These methods must be of Unit type, and only stub responses for the given mock; - `fun forgeSomething([args]): T`: methods setting up a forgery or an instance of a concrete class. These methods must return the forged instance; - `fun assertObjectMatchesCondition(object, [args])`: methods verifying that a given object matches a given condition. These methods must be of Unit type, and only call assertions with the AssertJ framework (or native assertions); - `fun verifyMockMatchesState(mock, [args])`: methods verifying that a mock’s interaction. These methods must be of Unit type, and only call verifications with the Mockito framework. - `fun setupSomething()`: method to setup a complex test (should only be used in the Given part of a test). +#### Clear vs Closed Box testing + +Clear Box testing is an approach to testing where the test knows +the implementation details of the production code. It usually involves making a class property visible +in the test (via the `internal` keyword instead of `private`). + +Closed Box testing on the contrary will only use `public` fields and +functions without checking the internal state of the object under test. + +While both can be useful, relying too much on Clear Box testing will make maintenance more complex: + + - the tiniest change in the production code will make the test break; + - Clear Box testing often leads to higher coupling and repeating the tested logic in the test class; + - it focuses more on the way the object under test works, and less on the behavior and usage. + +It is recommended to use Closed Box testing as much as possible. +#### Property Based Testing +To ensure that our tests cover the widest range of possible states and inputs, we use property based +testing thanks to the Elmyr library. Given a unit under test, we must make sure that the whole range +of possible input is covered for all tests. diff --git a/Database.db b/Database.db deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Dockerfile.gitlab b/Dockerfile.gitlab deleted file mode 100644 index d0a277184a..0000000000 --- a/Dockerfile.gitlab +++ /dev/null @@ -1,79 +0,0 @@ -FROM ubuntu:16.04 - -RUN apt-get update \ - && apt-get -y install openjdk-8-jdk \ - && rm -rf /var/lib/apt/lists/* - -RUN set -x \ - && apt-get update \ - && apt-get -y upgrade \ - && apt-get -y install --no-install-recommends \ - curl \ - git \ - unzip \ - wget \ - openssh-client \ - expect \ - && apt-get -y clean \ - && rm -rf /var/lib/apt/lists/* - -ENV GRADLE_VERSION 6.1.1 -ENV ANDROID_COMPILE_SDK 30 -ENV ANDROID_BUILD_TOOLS 30.0.2 -ENV ANDROID_SDK_TOOLS 4333796 -ENV NDK_VERSION 21.3.6528147 -ENV CMAKE_VERSION 3.10.2.4988404 - - - -RUN apt update && apt install -y python3 - -# Install pip for aws -RUN set -x \ - && curl -OL https://bootstrap.pypa.io/get-pip.py \ - && python3 get-pip.py \ - && rm get-pip.py - -RUN python3 --version - -RUN set -x \ - && pip install awscli - -# Gradle -RUN \ - cd /usr/local && \ - curl -L https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip -o gradle-${GRADLE_VERSION}-bin.zip && \ - unzip gradle-${GRADLE_VERSION}-bin.zip && \ - rm gradle-${GRADLE_VERSION}-bin.zip - -# Workaround for -# Warning: File /root/.android/repositories.cfg could not be loaded. -RUN mkdir /root/.android \ - && touch /root/.android/repositories.cfg - - -# Android SDK -RUN \ - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip && \ - unzip -d android-sdk-linux android-sdk.zip && \ - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null && \ - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null && \ - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null && \ - echo y | android-sdk-linux/tools/bin/sdkmanager --install "ndk;${NDK_VERSION}" >/dev/null && \ - echo y | android-sdk-linux/tools/bin/sdkmanager --install "cmake;${CMAKE_VERSION}" >/dev/null && \ - yes | android-sdk-linux/tools/bin/sdkmanager --licenses - -RUN set -x \ - && curl -OL https://s3.amazonaws.com/dd-package-public/dd-package.deb && dpkg -i dd-package.deb && rm dd-package.deb \ - && apt-get update \ - && apt-get -y clean \ - && rm -rf /var/lib/apt/lists/* - -ENV ANDROID_SDK_ROOT $PWD/android-sdk-linux -ENV ANDROID_HOME $PWD/android-sdk-linux -ENV GRADLE_HOME /usr/local/gradle-${GRADLE_VERSION} -ENV ANDROID_NDK $ANDROID_SDK_ROOT/ndk/${NDK_VERSION} -ENV PATH $PATH:$GRADLE_HOME/bin -ENV PATH $PATH:$ANDROID_HOME/platform-tools -ENV PATH $PATH:$ANDROID_SDK_ROOT/build-tools/${ANDROID_BUILD_TOOLS}:$ANDROID_NDK - diff --git a/LICENSE b/LICENSE index 41e4b0c61a..479ecdd260 100644 --- a/LICENSE +++ b/LICENSE @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 3c8562309d..2df2f0f941 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -6,6 +6,13 @@ import,androidx.arch.core,Apache-2.0,Copyright 2018 The Android Open Source Proj import,androidx.asynclayoutinflater,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.cardview,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.collection,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.constraintlayout,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.compose,Apache-2.0,Copyright 2019 The Android Open Source Project +import,androidx.compose.animation,Apache-2.0,Copyright 2019 The Android Open Source Project +import,androidx.compose.foundation,Apache-2.0,Copyright 2019 The Android Open Source Project +import,androidx.compose.material,Apache-2.0,Copyright 2019 The Android Open Source Project +import,androidx.compose.runtime,Apache-2.0,Copyright 2019 The Android Open Source Project +import,androidx.compose.ui,Apache-2.0,Copyright 2019 The Android Open Source Project import,androidx.coordinatorlayout,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.core,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.cursoradapter,Apache-2.0,Copyright 2018 The Android Open Source Project @@ -14,24 +21,35 @@ import,androidx.documentfile,Apache-2.0,Copyright 2018 The Android Open Source P import,androidx.drawerlayout,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.fragment,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.interpolator,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.leanback,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.legacy,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.lifecycle,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.loader,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.localbroadcastmanager,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.media,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.metrics,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.multidex,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.navigation,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.print,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.recyclerview,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.resourceinspection,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.room,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.savedstate,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.slidingpanelayout,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.sqlite,Apache-2.0,Copyright 2018 The Android Open Source Project +import,androidx.startup,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.swiperefreshlayout,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.transition,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.vectordrawable,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.versionedparcelable,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.viewpager,Apache-2.0,Copyright 2018 The Android Open Source Project import,androidx.work,Apache-2.0,Copyright 2018 The Android Open Source Project -import,com.facebook,MIT,Copyright (c) Facebook, Inc. and its affiliates +import,com.android.tools,Apache-2.0,Copyright 2018 The Android Open Source Project +import,com.apollographql.apollo,MIT,"Copyright (c) 2016-2024 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.)" +import,com.benasher44,MIT,"Copyright (c) 2019 Ben Asher" +import,com.github.bumptech.glide,"BSD 3-Clause","Copyright 2014 Google, Inc. All rights reserved, Copyright (c) 2013. Bump Technologies Inc. All Rights Reserved." +import,com.facebook.fresco,MIT,"Copyright (c) Facebook, Inc. and its affiliates" +import,com.github.spotbugs,"GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1","Copyright (C) 1991, 1999 Free Software Foundation, Inc." import,com.google.android.material,Apache-2.0,Copyright 2018 The Android Open Source Project import,com.google.code.gson,Apache-2.0,Copyright 2008 Google Inc import,com.google.guava,Apache-2.0,Copyright 2009 The Guava Authors @@ -40,37 +58,113 @@ import,com.lyft.kronos,Apache-2.0,Copyright (C) 2018 Lyft Inc. import,com.squareup.okhttp3,Apache-2.0,"Copyright 2019 Square, Inc" import,com.squareup.okio,Apache-2.0,"Copyright 2013 Square, Inc" import,com.squareup.moshi,Apache-2.0,"Copyright 2013 Square, Inc" -import,com.fasterxml.jackson,Apache-2.0,Copyright 2020 Datadog, Inc. -import,com.datadoghq,Apache-2.0,Copyright 2017 Datadog, Inc +import,com.squareup.sqldelight,Apache-2.0,"Copyright 2016 Square, Inc" +import,com.fasterxml.jackson,Apache-2.0,"Copyright 2020 Datadog, Inc." +import,com.datadoghq,Apache-2.0,"Copyright 2017 Datadog, Inc" +import,io.coil-kt,Apache-2.0,Copyright 2021 Coil Contributors +import,io.opentelemetry,Apache-2.0,Copyright 2019 The OpenTelemetry Authors import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors import,io.opentracing.contrib,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors -import,io.reactivex.rxjava3,Apache-2.0,Copyright (c) 2016-present, RxJava Contributors +import,io.reactivex.rxjava3,Apache-2.0,"Copyright (c) 2016-present, RxJava Contributors" import,io.reactivex.rxjava3.android,Apache-2.0,Copyright 2015 The RxAndroid authors import,org.jetbrains,Apache-2.0,Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors import,org.jetbrains.kotlin,Apache-2.0,Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors import,org.jetbrains.kotlinx,Apache-2.0,Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors +import,org.reactivestreams,CC0,Copyright 2014 Reactive Streams +import(test),androidx.autofill,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.concurrent,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.dynamicanimation,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.emoji2,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.exifinterface,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.exifinterface,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.profileinstaller,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test.espresso,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test.espresso,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test.ext,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test.ext,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test.services,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.test.services,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.tracing,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.tracing,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.viewpager2,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.viewpager2,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),androidx.window,Apache-2.0,Copyright 2018 The Android Open Source Project import(test),com.android.support,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),com.facebook.soloader,Apache-2.0,"Copyright (c) Facebook, Inc. and its affiliates" import(test),com.github.xgouchet.Elmyr,MIT,Copyright 2017-2019 Xavier F. Gouchet -import(test),com.nhaarman.mockitokotlin2,MIT,"Copyright (c) 2018 Niek Haarman, Copyright (c) 2007 Mockito contributors" +import(test),com.google.android.apps.common.testing.accessibility.framework,Apache-2.0,Copyright 2018 The Android Open Source Project +import(test),com.google.code.findbugs,Apache-2.0,"Copyright (C) 2006, University of Maryland" +import(test),com.google.errorprone,Apache-2.0,"Copyright 2018 The Error Prone Authors" +import(test),com.google.j2objc,Apache-2.0,"Copyright (C) 2011 The Android Open Source Project" +import(test),com.google.protobuf,BSD-3-Clause,"Copyright 2008 Google Inc" +import(test),com.google.re2j,"Go License","Copyright (c) 2009 The Go Authors. All rights reserved." +import(test),com.parse.bolts,"BSD License","Copyright (c) Facebook, Inc. and its affiliates." +import(test),com.squareup,Apache-2.0,"Copyright 2015 Square, Inc." +import(test),greatest,ICT,"Copyright (c) 2011-2018 Scott Vokes " +import(test),javax.inject,Apache-2.0,Copyright (C) 2009 The JSR-330 Expert Group import(test),junit,EPL-1.0,Copyright © 2002-2019 JUnit import(test),net.bytebuddy,Apache-2.0,Copyright 2014 - 2019 Rafael Winterhalter import(test),net.sf.kxml,MIT,"Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany" import(test),net.wuerl.kotlin,Apache-2.0,Copyright 2016 Andreas Würl import(test),org.apiguardian,Apache-2.0,Copyright 2002-2017 the original author or authors import(test),org.assertj,Apache-2.0,Copyright 2012-2019 the original author or authors +import(test),org.ccil.cowan.tagsoup,Apache-2.0,Copyright 2002-2008 by John Cowan +import(test),org.checkerframework,"MIT","Copyright 2004-present by the Checker Framework developers" import(test),org.hamcrest,BSD-3-Clause,Copyright (c) 2000-2015 www.hamcrest.org -import(test),org.jacoco,EPL-2.0,Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors +import(test),org.jacoco,EPL-2.0,"Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors" +import(test),org.jctools,"Apache-2.0","Copyright 2023 Datadog, Inc." +import(test),org.jsoup,MIT,"Copyright (c) 2009-2024 Jonathan Hedley" import(test),org.junit,EPL-2.0,Copyright 2015-2019 the original author or authors import(test),org.junit.jupiter,EPL-2.0,Copyright 2015-2019 the original author or authors import(test),org.junit.platform,EPL-2.0,Copyright 2015-2019 the original author or authors import(test),org.junit.vintage,EPL-2.0,Copyright 2015-2019 the original author or authors import(test),org.mockito,MIT,Copyright (c) 2007 Mockito contributors +import(test),org.mockito.kotlin,MIT,"Copyright (c) 2016 Niek Haarman, Copyright (c) 2007 Mockito contributors" import(test),org.objenesis,Apache-2.0,"Copyright (c) 2003-2013, Objenesis Team and all contributors" import(test),org.opentest4j,Apache-2.0,Copyright 2015-2018 the original author or authors import(test),org.robolectric,Apache-2.0,Copyright 2015-2018 the original author or authors +import(test),org.sonatype.oss,Apache-2.0,"Copyright (c) 2008-present Sonatype, Inc." +import(test),uk.org.webcompere,MIT,Copyright (c) 2020 Ashley Frieze (c) 2017 Stefan Birkner +build,androidx.compose.compiler,Apache-2.0,Copyright 2019 The Android Open Source Project +build,ch.qos.logback,EPL-1.0,"Copyright (C) 1999-2015, QOS.ch" build,com.android.tools.build,Apache-2.0,Copyright (C) 2013 The Android Open Source Project +build,com.android.tools.ddms,Apache-2.0,Copyright (C) 2013 The Android Open Source Project +build,com.android.tools.emulator,Apache-2.0,Copyright (C) 2013 The Android Open Source Project build,com.android.tools.lint,Apache-2.0,Copyright (C) 2013 The Android Open Source Project +build,com.android.tools.utp,Apache-2.0,Copyright (C) 2013 The Android Open Source Project +build,com.fasterxml.jackson.core,Apache-2.0,"Copyright (c) 2007- Tatu Saloranta" +build,com.fasterxml.jackson.dataformat,Apache-2.,"Copyright (c) 2007- Tatu Saloranta" +build,com.fasterxml.jackson.module,Apache-2.0,"Copyright (c) 2007- Tatu Saloranta" +build,com.fasterxml.woodstox,Apache-2.0,"Copyright (c) 2007- Tatu Saloranta" +build,com.google.android,Apache-2.0,Copyright (C) 2013 The Android Open Source Project +build,com.google.api.grpc,Apache-2.0,Copyright 2020 Google LLC +build,com.google.auto,Apache-2.0,"Copyright 2014 Google LLC" +build,com.google.auto.service,Apache-2.0,"Copyright 2013 Google LLC" +build,com.google.crypto.tink,Apache-2.0,Copyright 2023 Google LLC +build,com.google.dagger,Apache-2.0,"Copyright (C) 2018 The Dagger Authors" +build,com.google.devtools.ksp,Apache-2.0,"Copyright 2020 Google LLC, Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors." +build,com.google.testing.platform,Apache-2.0,Copyright (C) 2021 The Android Open Source Project build,com.pinterest,MIT,"Copyright 2019 Pinterest Inc, Copyright 2016-2019 Stanley Shyiko" +build,com.soywiz.korlibs.korte,MIT,Copyright (c) 2017-2019 Carlos Ballesteros Velasco and contributors +build,commons-io,Apache-2.0,Copyright 2002-2024 The Apache Software Foundation +build,io.github.microutils,Apache-2.0,Copyright (c) 2016-2018 Ohad Shai build,io.gitlab.arturbosch.detekt,Apache-2.0,Copyright 2016-2019 the original author or authors +build,io.grpc,Apache-2.0,Copyright 2014 The gRPC Authors +build,io.netty,Apache-2.0,Copyright 2014 The Netty Project +build,io.opencensus,Apache-2.0,"Copyright 2017, OpenCensus Authors" +build,io.perfmark,Apache-2.0,Copyright 2019 Google LLC +build,jakarta.activation,BSD 3-Clause,"Copyright (c) 2017, 2018 Oracle and/or its affiliates" +build,jakarta.xml.bind,BSD 3-Clause,"Copyright (c) 2017, 2018 Oracle and/or its affiliates" +build,javax.annotation,"CDDL + GPLv2 with classpath exception",Copyright (c) 2009-2024 Oracle and/or its affiliates +build,net.java.dev.jna,"Apache-2.0","Copyright (c) 2007 Timothy Wall, All Rights Reserved" +build,org.codehaus.mojo,MIT,"Copyright (c) 2009, codehaus.org" +build,org.codehaus.woodstox,BSD 2-Clause,"Copyright (c) 2008 FasterXML LLC" +build,org.ec4j.core,Apache-2.0,"Copyright (c) 2017 Angelo Zerr and other contributors" +build,org.freemarker,Apache-2.0,"Copyright 2015-2018 The Apache Software Foundation" build,org.jetbrains.dokka,Apache-2.0,"Copyright 2014-2019 JetBrains s.r.o. and Dokka project contributors." -test,greatest,ICT,"Copyright (c) 2011-2018 Scott Vokes " +build,org.jetbrains.intellij.deps,LGPL-2.1-only,"Copyright (c) 2001-2002, Eric D. Friedman, Jason Baldridge, Copyright (c) 1999 CERN - European Organization for Nuclear Research" +build,org.ow2.asm,BSD-3-Clause,"Copyright (c) 2000-2011 INRIA, France Telecom" +build,org.slf4j,MIT,"Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland)" + diff --git a/MIGRATION.MD b/MIGRATION.MD new file mode 100644 index 0000000000..d4dbf3321e --- /dev/null +++ b/MIGRATION.MD @@ -0,0 +1,474 @@ +# Migration from 2.x to 3.0 + +The main changes introduced in SDK 3.0 compared to 2.x are: + +1. The minimum supported Android API version increased to `23`. + +We now follow Google's [AndroidX library version policy](https://developer.android.com/jetpack/androidx/versions#version-table). + This update lets us use the latest `AndroidX` libraries, including +[androidx.metrics:metrics-performance](https://developer.android.com/jetpack/androidx/releases/metrics). + +2. Fatal crashes are no longer automatically reported to the Logs feature. + +Follow +these [instructions](https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/android/error_tracking) +to enable crash reporting with RUM. + +3. The `OpenTracing` dependency was removed because it is obsolete. + +We replaced the `OpenTracing` classes with internal ones. However, we strongly encourage you to use the `OpenTelemetry` version as the new tracing API standard. + + +### Trace product changes + +#### Migrating tracing from `OpenTracing` to `OpenTelemetry` (recommended) + +> [!WARNING] +> Note that the `OpenTelemetry` specification library [requires](https://github.com/open-telemetry/opentelemetry-java?tab=readme-ov-file#requirements) desugaring to be enabled for projects with a `minSdk` < 26. If this requirement cannot be met in your project see the following section. + + +1. Add the `OpenTelemetry` dependency to your `build.gradle.kts`: + +```kotlin +implementation(project("com.datadoghq:dd-sdk-android-trace-otel:x.x.x")) +``` + +2. Replace the `OpenTracing` configuration: +```kotlin + GlobalTracer.registerIfAbsent( + AndroidTracer.Builder() + .setService(BuildConfig.APPLICATION_ID) + .build() + ) +``` + +with the `OpenTelemetry` configuration: +```kotlin + GlobalOpenTelemetry.set(object : OpenTelemetry { + private val tracerProvider = OtelTracerProvider.Builder() + .setService(BuildConfig.APPLICATION_ID) + .build() + + override fun getTracerProvider(): TracerProvider { + return tracerProvider + } + + override fun getPropagators(): ContextPropagators { + return ContextPropagators.noop() + } + }) +``` + +> [!NOTE] +> You can use the default configuration to make it even shorter in case if don't provide any additional parameters: +```kotlin + GlobalOpenTelemetry.set( + DatadogOpenTelemetry(BuildConfig.APPLICATION_ID) + ) +``` + +To access the tracer object for manual (custom) tracing, use `io.opentelemetry.api.GlobalOpenTelemetry.get()` instead of `io.opentracing.util.GlobalTracer.get()`. +For example: +```kotlin + val tracer: Tracer = GlobalOpenTelemetry + .get() + .getTracer("SampleApplicationTracer") + + val span = tracer + .spanBuilder("Executing operation") + .startSpan() + + // Code that should be instrumented + + span.end() +``` + +Refer to the official `OpenTelemetry` [documentation](https://opentelemetry.io/docs/) for more details. + +#### Migrating tracing from `OpenTracing` to `DatadogTracing` (transition period) + +> [!WARNING] +> This option has been added for compatibility and to simplify the transition from `OpenTracing` to `OpenTelemetry`, but it may not be available in future major releases. We recommend using `OpenTelemetry` as the standard for tracing tasks. However, if it is not possible to enable desugaring in your project for some reason, you can use this method. + +Replace the `OpenTracing` configuration: +```kotlin + GlobalTracer.registerIfAbsent( + AndroidTracer.Builder() + .setService(BuildConfig.APPLICATION_ID) + .build() + ) +``` + +with the `DatadogTracing` configuration: +```kotlin + GlobalDatadogTracer.registerIfAbsent( + DatadogTracing.newTracerBuilder() + .build() + ) +``` + +For manual (custom) tracing use `com.datadog.android.trace.GlobalDatadogTracer.get()` instead of `io.opentracing.util.GlobalTracer.get()` to access the tracer object. +For example: +```kotlin + val tracer = GlobalDatadogTracer.get() + + val span = tracer + .buildSpan("Executing operation") + .start() + + // Code that should be instrumented + + span.finish() +``` +Refer to the Datadog [documentation](https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/android?tab=kotlin) for more details. + +API changes: + +|`2.x`|`3.0` `OpenTelemetry`|`3.0` `DatadogTracing`| +|---|---|---| +|`io.opentracing.util.GlobalTracer`|`io.opentelemetry.api.GlobalOpenTelemetry`|`com.datadog.android.trace.GlobalDatadogTracer`| +|`com.datadog.android.trace.AndroidTracer`|`io.opentelemetry.api.trace.Tracer`|`com.datadog.android.trace.api.tracer.DatadogTracer`| +|`io.opentracing.Span`|`io.opentelemetry.api.trace.Span`|`com.datadog.android.trace.api.span.DatadogSpan`| +|`io.opentracing.Scope`|`io.opentelemetry.context.Scope`|`com.datadog.android.trace.api.scope.DatadogScope`| +|`io.opentracing.SpanContext`|`io.opentelemetry.api.trace.SpanContext`|`com.datadog.android.trace.api.span.DatadogSpanContext`| + +Replacement hints: + +|`2.x`|`3.0` `OpenTelemetry`|`3.0` `DatadogTracing`| +|---|---|---| +|`AndroidTracer.Builder().build()`||`DatadogTracing.newTracerBuilder().build()`| +|`AndroidTracer#setPartialFlushThreshold(Int)`|`OtelTracerProvider#setPartialFlushThreshold()`|`DatadogTracerBuilder#withPartialFlushMinSpans()`| +|`io.opentracing.SpanContext#toTraceId()`|`io.opentelemetry.api.trace.SpanContext#getTraceId()`|`DatadogSpanContext.traceId.toString()`| +|`io.opentracing.Span#setError()`|`io.opentelemetry.api.trace#recordException()`|`DatadogSpan#addThrowable()`| + +### OkHttp instrumentation changes + +The OkHttp instrumentation (`com.datadoghq:dd-sdk-android-okhttp:x.x.x`) doesn't require desugaring support. However few migration actions may be necessary. + + +API changes: + +|`2.x`|`3.0`| +|--|--| +|`TracingInterceptor(String, List, TracedRequestListener,Sampler)`| Use `TracingInterceptor.Builder()` instead.| +|`TracingInterceptor(String?,Map>, TracedRequestListener, Sampler)`| Use `TracingInterceptor.Builder()` instead.| +|`TracingInterceptor(String?,TracedRequestListener,Sampler)`|Use `TracingInterceptor.Builder()` instead.| +|`DatadogInterceptor(String?, Map>,TracedRequestListener, RumResourceAttributesProvider, Sampler)`|Use `DatadogInterceptor.Builder()` instead. | +|`DatadogInterceptor(String?,List,TracedRequestListener,RumResourceAttributesProvider,Sampler)`|Use `DatadogInterceptor.Builder()` instead.| +| `DatadogInterceptor(String?,TracedRequestListener,RumResourceAttributesProvider,Sampler) ` | Use `DatadogInterceptor.Builder()` instead. | + + +### Core product changes + +API changes: + +|`2.x`| `3.0` | +|--|------------------------------------------------------------------| +|`Datadog#setUserInfo(String?, ...)`| User info ID is now mandatory `Datadog.setUserInfo(String, ...)` | + + +### RUM product changes + +API changes: + +|`2.x`| `3.0` | +|--|-------------------------------------------------------------------------------------| +|`DatadogRumMonitor#startResource(String, String, String,Map)`| Use `startResource` method which takes `RumHttpMethod` as `method` parameter instead | +|`com.datadog.android.rum.GlobalRum`|`GlobalRum` object was renamed to `com.datadog.android.rum.GlobalRumMonitor` | +|`com.datadog.android.rum.RumMonitor#addAction()`| Parameter `attributes: Map` is now optional | +|`com.datadog.android.rum.RumMonitor#startAction()`| Parameter `attributes: Map` is now optional | +|`com.datadog.android.rum.RumMonitor#stopResource()`| Parameter `attributes: Map` is now optional | +|`com.datadog.android.rum.RumMonitor#addError()`| Parameter `attributes: Map` is now optional | +|`com.datadog.android.rum.RumMonitor#addErrorWithStacktrace()`| Parameter `attributes: Map` is now optional | +|`com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor#stopResource()`| Parameter `attributes: Map` is now optional | +|`com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor#stopResource()`| Parameter `attributes: Map` is now optional | + +# Migration from 1.x to 2.0 + +The main changes introduced in SDK 2.0 compared to 1.x are: + +1. All relevant products (RUM, Trace, Logs, etc.) are now extracted into different modules. That allows you to integrate only what is needed into your application. + +Whereas all products in version 1.x were contained in the single artifact `com.datadoghq:dd-sdk-android:x.x.x`, you now need to adopt the following artifacts: + +* RUM: `com.datadoghq:dd-sdk-android-rum:x.x.x` +* Logs: `com.datadoghq:dd-sdk-android-logs:x.x.x` +* Trace: `com.datadoghq:dd-sdk-android-trace:x.x.x` +* WebView Tracking: `com.datadoghq:dd-sdk-android-webview:x.x.x` +* OkHttp instrumentation: `com.datadoghq:dd-sdk-android-okhttp:x.x.x` + +**Note**: If you utilize NDK Crash Reporting and WebView Tracking, you also need to add RUM and/or Logs artifacts to be able to report events to RUM and/or Logs respectively. + +Reference to the `com.datadoghq:dd-sdk-android` artifact should be removed from your Gradle buildscript, this artifact doesn't exist anymore. + +**Note**: The Maven coordinates of all the other artifacts stay the same. + +2. Support for multiple SDK instances (see below). +3. Unification of the API layout, as well as naming between iOS and Android SDKs with other Datadog products. Datadog SDK v2 is not binary compatible with Datadog SDK v1. +4. Support of Android API 19 (KitKat) was dropped. The minimum SDK supported is now API 21 (Lollipop). +5. Kotlin 1.7 is required in order to integrate the SDK. SDK itself is compiled with Kotlin 1.8, so compiler of Kotlin 1.6 and below cannot read SDK classes metadata. + +If you have an error like the following: + +``` +A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable +Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and kotlin-stdlib-jdk8-1.7.20 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20) +``` + +you need to add the following rules to your buildscript (more details [here](https://stackoverflow.com/a/75298544)): + +```kotlin +dependencies { + constraints { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10") { + because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") + } + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10") { + because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") + } + } +} +``` + +You can always refer to this [sample application](https://github.com/DataDog/dd-sdk-android/blob/9c2d460b6b66161efb1252039a82784792958042/sample/kotlin/src/main/kotlin/com/datadog/android/sample/SampleApplication.kt) for an example on how to setup the SDK. + +## SDK configuration changes + +Better SDK granularity is achieved with the extraction of different products into independent modules. This changes the way SDK is configured. + +`com.datadog.android.core.configuration.Configuration.Builder` class has the following changes: + +* Client token, env name, variant name (default value is empty string), service name (default value is application ID taken from the manifest) should be provided in the constructor. +* `com.datadog.android.core.configuration.Credentials` class which was containing parameters mentioned above is removed. +* `logsEnabled`, `tracesEnabled`, `rumEnabled` are removed from the constructor in favour of the individual products configuration (see below). +* `crashReportsEnabled` constructor argument is removed. You can enable/disable JVM crash reporting by using `Configuration.Builder.setCrashReportsEnabled` method, by default JVM crash reporting is enabled. +* RUM, Logs, Trace products configuration methods are removed from `Configuration.Builder` in favour of the individual products configuration (see below). + +`Datadog.initialize` method has `Credentials` class removed from the list of the arguments. + +`com.datadog.android.plugin` package and all related classes/methods are removed. + +### Logs product changes + +All the classes related to the Logs product are now strictly contained in the `com.datadog.android.log` package. + +To use Logs product, import the following artifact: + +```kotlin +implementation("com.datadoghq:dd-sdk-android-logs:x.x.x") +``` + +You can enable the Logs product with the following snippet: + +```kotlin +val logsConfig = LogsConfiguration.Builder() + ... + .build() + +Logs.enable(logsConfig) + +val logger = Logger.Builder() + ... + .build() +``` + +API changes: + +|`1.x`|`2.0`| +|---|---| +|`com.datadog.android.core.configuration.Configuration.Builder.setLogEventMapper`|`com.datadog.android.log.LogsConfiguration.Builder.setEventMapper`| +|`com.datadog.android.core.configuration.Configuration.Builder.useCustomLogsEndpoint`|`com.datadog.android.log.LogsConfiguration.Builder.useCustomEndpoint`| +|`com.datadog.android.log.Logger.Builder.setLoggerName`|`com.datadog.android.log.Logger.Builder.setName`| +|`com.datadog.android.log.Logger.Builder.setSampleRate`|`com.datadog.android.log.Logger.Builder.setRemoteSampleRate`| +|`com.datadog.android.log.Logger.Builder.setDatadogLogsEnabled`|This method has been removed. Use `com.datadog.android.log.Logger.Builder.setRemoteSampleRate(0f)` instead to disable sending logs to Datadog.| +|`com.datadog.android.log.Logger.Builder.setServiceName`|`com.datadog.android.log.Logger.Builder.setService`| +|`com.datadog.android.log.Logger.Builder.setDatadogLogsMinPriority`|`com.datadog.android.log.Logger.Builder.setRemoteLogThreshold`| + +### Trace product changes + +All the classes related to the Trace product are now strictly contained in the `com.datadog.android.trace` package (this means that all classes residing in `com.datadog.android.tracing` before have moved). + +To use the Trace product, import the following artifact: + +```kotlin +implementation("com.datadoghq:dd-sdk-android-trace:x.x.x") +``` + +Enable the Trace product with the following snippet: + +```kotlin +val traceConfig = TraceConfiguration.Builder() + ... + .build() + +Trace.enable(traceConfig) + +val tracer = AndroidTracer.Builder() + ... + .build() + +GlobalTracer.registerIfAbsent(tracer) +``` + +API changes: + +|`1.x`|`2.0`| +|---|---| +|`com.datadog.android.core.configuration.Configuration.Builder.setSpanEventMapper`|`com.datadog.android.trace.TraceConfiguration.Builder.setEventMapper`| +|`com.datadog.android.core.configuration.Configuration.Builder.useCustomTracesEndpoint`|`com.datadog.android.trace.TraceConfiguration.Builder.useCustomEndpoint`| +|`com.datadog.android.tracing.AndroidTracer.Builder.setSamplingRate`|`com.datadog.android.trace.AndroidTracer.Builder.setSampleRate`| +|`com.datadog.android.tracing.AndroidTracer.Builder.setServiceName`|`com.datadog.android.trace.AndroidTracer.Builder.setService`| + +### RUM product changes + +All classes related to the RUM product are now strictly contained in the `com.datadog.android.rum` package. + +To use the RUM product, import the following artifact: + +```kotlin +implementation("com.datadoghq:dd-sdk-android-rum:x.x.x") +``` + +The RUM product can be enabled with the following snippet: + +```kotlin +val rumConfig = RumConfiguration.Builder(rumApplicationId) + ... + .build() + +Rum.enable(rumConfig) +``` + +API changes: + +|`1.x`|`2.0`| +|---|---| +|`com.datadog.android.core.configuration.Configuration.Builder.setRumViewEventMapper`|`com.datadog.android.rum.RumConfiguration.Builder.setViewEventMapper`| +|`com.datadog.android.core.configuration.Configuration.Builder.setRumResourceEventMapper`|`com.datadog.android.rum.RumConfiguration.Builder.setResourceEventMapper`| +|`com.datadog.android.core.configuration.Configuration.Builder.setRumActionEventMapper`|`com.datadog.android.rum.RumConfiguration.Builder.setActionEventMapper`| +|`com.datadog.android.core.configuration.Configuration.Builder.setRumErrorEventMapper`|`com.datadog.android.rum.RumConfiguration.Builder.setErrorEventMapper`| +|`com.datadog.android.core.configuration.Configuration.Builder.setRumLongTaskEventMapper`|`com.datadog.android.rum.RumConfiguration.Builder.setLongTaskEventMapper`| +|`com.datadog.android.core.configuration.Configuration.Builder.useCustomRumEndpoint`|`com.datadog.android.rum.RumConfiguration.Builder.useCustomEndpoint`| +|`com.datadog.android.event.ViewEventMapper`|`com.datadog.android.rum.event.ViewEventMapper`| +|`com.datadog.android.core.configuration.VitalsUpdateFrequency`|`com.datadog.android.rum.configuration.VitalsUpdateFrequency`| +|`com.datadog.android.core.configuration.Configuration.Builder.trackInteractions`|`com.datadog.android.rum.RumConfiguration.Builder.trackUserInteractions`| +|`com.datadog.android.core.configuration.Configuration.Builder.disableInteractionTracking`|`com.datadog.android.rum.RumConfiguration.Builder.disableUserInteractionTracking`| +|`com.datadog.android.core.configuration.Configuration.Builder.sampleRumSessions`|`com.datadog.android.rum.RumConfiguration.Builder.setSessionSampleRate`| +|`com.datadog.android.core.configuration.Configuration.Builder.sampleTelemetry`|`com.datadog.android.rum.RumConfiguration.Builder.setTelemetrySampleRate`| +|`com.datadog.android.rum.RumMonitor.Builder`|This class has been removed. The RUM monitor is created and registered during the `Rum.enable` call.| +|`com.datadog.android.rum.RumMonitor.Builder.sampleRumSessions`|`com.datadog.android.rum.RumConfiguration.Builder.setSessionSampleRate`| +|`com.datadog.android.rum.RumMonitor.Builder.setSessionListener`|`com.datadog.android.rum.RumConfiguration.Builder.setSessionListener`| +|`com.datadog.android.rum.RumMonitor.addUserAction`|`com.datadog.android.rum.RumMonitor.addAction`| +|`com.datadog.android.rum.RumMonitor.startUserAction`|`com.datadog.android.rum.RumMonitor.startAction`| +|`com.datadog.android.rum.RumMonitor.stopUserAction`|`com.datadog.android.rum.RumMonitor.stopAction`| +|`com.datadog.android.rum.GlobalRum.registerIfAbsent`|This method has been removed. The RUM monitor is created and registered during the `Rum.enable` call.| +|`com.datadog.android.rum.GlobalRum`|`com.datadog.android.rum.GlobalRumMonitor`| +|`com.datadog.android.rum.GlobalRum.addAttribute`|`com.datadog.android.rum.RumMonitor.addAttribute`| +|`com.datadog.android.rum.GlobalRum.removeAttribute`|`com.datadog.android.rum.RumMonitor.removeAttribute`| + +### NDK Crash Reporting changes + +The artifact name stays the same as before: `com.datadoghq:dd-sdk-android-ndk:x.x.x` + +NDK Crash Reporting can be enabled using the following snippet: + +```kotlin +NdkCrashReports.enable() +``` + +This configuration replaces the `com.datadog.android.core.configuration.Configuration.Builder.addPlugin` call used before. + +**Note**: You should have RUM and/or Logs products enabled in order to receive NDK crash reports in RUM and/or Logs. + +### WebView Tracking changes + +The artifact name stays the same as before: `com.datadoghq:dd-sdk-android-webview:x.x.x` + +You can enable WebView Tracking with the following snippet: + +```kotlin +WebViewTracking.enable(webView, allowedHosts) +``` + +**Note**: You should have RUM and/or Logs products enabled in order to receive events coming from WebView in RUM and/or Logs. + +API changes: + +|`1.x`|`2.0`| +|---|---| +|`com.datadog.android.webview.DatadogEventBridge`|This method became an `internal` class. Use `WebViewTracking` instead.| +|`com.datadog.android.rum.webview.RumWebChromeClient`|This class was removed. Use `WebViewTracking` instead.| +|`com.datadog.android.rum.webview.RumWebViewClient`|This class was removed. Use `WebViewTracking` instead.| + +### OkHttp Tracking changes + +In order to be able to use OkHttp Tracking you need to import the following artifact: + +```kotlin +implementation("com.datadoghq:dd-sdk-android-okhttp:x.x.x") +``` + +OkHttp instrumentation now supports the case when Datadog SDK is initialized after the OkHttp client, allowing you to create `com.datadog.android.okhttp.DatadogEventListener`, `com.datadog.android.okhttp.DatadogInterceptor`, and `com.datadog.android.okhttp.trace.TracingInterceptor` before Datadog SDK. OkHttp instrumentation starts reporting events to Datadog once Datadog SDK is initialized. + +Also, both `com.datadog.android.okhttp.DatadogInterceptor` and `com.datadog.android.okhttp.trace.TracingInterceptor` improve the integration with remote configuration, allowing you to control sampling dynamically. +In order to do that, you need to provide your own implementation of the `com.datadog.android.core.sampling.Sampler` interface in the `com.datadog.android.okhttp.DatadogInterceptor`/`com.datadog.android.okhttp.trace.TracingInterceptor` constructor. It is queried for each request to make the sampling decision. + +### `dd-sdk-android-ktx` module removal + +In order to provide the better granularity for the Datadog SDK libraries used, `dd-sdk-android-ktx` module which was containing extension methods for both RUM and Trace features is removed and the code was re-arranged between the other modules: + +| `1.x` | '2.0' | Module name | +|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|-----------------------------------| +| `com.datadog.android.ktx.coroutine#kotlinx.coroutines.CoroutineScope.launchTraced` | `com.datadog.android.trace.coroutines#kotlinx.coroutines.CoroutineScope.launchTraced` | `dd-sdk-android-trace-coroutines` | +| `com.datadog.android.ktx.coroutine#runBlockingTraced` | `com.datadog.android.trace.coroutines#runBlockingTraced` | `dd-sdk-android-trace-coroutines` | +| `com.datadog.android.ktx.coroutine#kotlinx.coroutines.CoroutineScope.asyncTraced` | `com.datadog.android.trace.coroutines#kotlinx.coroutines.CoroutineScope.asyncTraced` | `dd-sdk-android-trace-coroutines` | +| `com.datadog.android.ktx.coroutine#kotlinx.coroutines.Deferred.awaitTraced` | `com.datadog.android.trace.coroutines#kotlinx.coroutines.Deferred.awaitTraced` | `dd-sdk-android-trace-coroutines` | +| `com.datadog.android.ktx.coroutine#withContextTraced` | `com.datadog.android.trace.coroutines#withContextTraced` | `dd-sdk-android-trace-coroutines` | +| `com.datadog.android.ktx.coroutine.CoroutineScopeSpan` | `com.datadog.android.trace.coroutines.CoroutineScopeSpan` | `dd-sdk-android-trace-coroutines` | +| `com.datadog.android.ktx.sqlite#android.database.sqlite.SQLiteDatabase.transactionTraced` | `com.datadog.android.trace.sqlite#android.database.sqlite.SQLiteDatabase.transactionTraced` | `dd-sdk-android-trace` | +| `com.datadog.android.ktx.tracing#io.opentracing.Span.setError` | `com.datadog.android.trace#io.opentracing.Span.setError` | `dd-sdk-android-trace` | +| `com.datadog.android.ktx.tracing#withinSpan` | `com.datadog.android.trace#withinSpan` | `dd-sdk-android-trace` | +| `com.datadog.android.ktx.coroutine#sendErrorToDatadog` | `com.datadog.android.rum.coroutines#sendErrorToDatadog` | `dd-sdk-android-rum-coroutines` | +| `com.datadog.android.ktx.rum#java.io.Closeable.useMonitored` | `com.datadog.android.rum#java.io.Closeable.useMonitored` | `dd-sdk-android-rum` | +| `com.datadog.android.ktx.rum#android.content.Context.getAssetAsRumResource` | `com.datadog.android.rum.resource#android.content.Context.getAssetAsRumResource` | `dd-sdk-android-rum` | +| `com.datadog.android.ktx.rum#android.content.Context.getRawResAsRumResource` | `com.datadog.android.rum.resource#android.content.Context.getRawResAsRumResource` | `dd-sdk-android-rum` | +| `com.datadog.android.ktx.rum#java.io.InputStream.asRumResource` | `com.datadog.android.rum.resource#java.io.InputStream.asRumResource` | `dd-sdk-android-rum` | +| `com.datadog.android.ktx.tracing#okhttp3.Request.Builder.parentSpan` | `com.datadog.android.okhttp.trace#okhttp3.Request.Builder.parentSpan` | `dd-sdk-android-okhttp` | + +## Using a Secondary Instance of the SDK + +Previously, the Datadog SDK implemented a singleton and only one SDK instance could exist in the application process. This created obstacles for use-cases like the usage of the SDK by 3rd party libraries. + +With version 2.0 we addressed this limitation: + +* It is now possible to initialize multiple instances of the SDK by associating them with a name. +* Many methods of the SDK can optionally take an SDK instance as an argument. If not provided, the call is associated with the default (nameless) SDK instance. + +Here is an example illustrating how to initialize a secondary core instance and enable products: + +```kotlin +val namedSdkInstance = Datadog.initialize("myInstance", context, configuration, trackingConsent) +val userInfo = UserInfo(...) +Datadog.setUserInfo(userInfo, sdkCore = namedSdkInstance) + +Logs.enable(logsConfig, namedSdkInstance) +val logger = Logger.Builder(namedSdkInstance) + ... + .build() + +Trace.enable(traceConfig, namedSdkInstance) +val tracer = AndroidTracer.Builder(namedSdkInstance) + ... + .build() + +Rum.enable(rumConfig, namedSdkInstance) +GlobalRumMonitor.get(namedSdkInstance) + +NdkCrashReports.enable(namedSdkInstance) + +WebViewTracking.enable(webView, allowedHosts, namedSdkInstance) +``` + +**Note**: The SDK instance name should have the same value between application runs. Storage paths for SDK events are associated with it. + +You can retrieve the named SDK instance by calling `Datadog.getInstance()` and use the `Datadog.isInitialized()` method to check if the particular SDK instance is initialized. \ No newline at end of file diff --git a/README.md b/README.md index e94929baac..d852a6ab47 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,52 @@ -# Datadog SDK for Android +# Datadog SDK for Android and Android TV -> A client-side Android library to interact with Datadog. +> A client-side Android and Android TV library to interact with Datadog. ## Getting Started ### Log Collection -See the dedicated [Datadog Android Log Collection documentation](http://docs.datadoghq.com/logs/log_collection/android) to learn how to forward logs from your Android application to Datadog. +See the dedicated [Datadog Android Log Collection documentation][1] to learn how to forward logs from your Android or Android TV application to Datadog. ### Real User Monitoring -See the dedicated [Datadog Android RUM Collection documentation](https://docs.datadoghq.com/real_user_monitoring/android/) to learn how to send RUM data from your Android application to Datadog. +See the dedicated [Datadog Android RUM Collection documentation][2] to learn how to send RUM data from your Android or Android TV application to Datadog. ## Log Integrations ### Timber -If your existing codebase is using Timber, you can forward all those logs to Datadog automatically by using the [dedicated library](timber_integration.md). +If your existing codebase is using Timber, you can forward all those logs to Datadog automatically by using the [dedicated library](integrations/dd-sdk-android-timber/README.md). ## RUM Integrations ### Coil -If you use Coil to load images in your application, take a look at Datadog's [dedicated library](dd-sdk-android-coil/README.md). +If you use Coil to load images in your application, see Datadog's [dedicated library](integrations/dd-sdk-android-coil/README.md). ### Fresco -If you use Fresco to load images in your application, take a look at Datadog's [dedicated library](dd-sdk-android-fresco/README.md). +If you use Fresco to load images in your application, see Datadog's [dedicated library](integrations/dd-sdk-android-fresco/README.md). ### Glide -If you use Glide to load images in your application, take a look at our [dedicated library](dd-sdk-android-glide/README.md). +If you use Glide to load images in your application, see Datadog's [dedicated library](integrations/dd-sdk-android-glide/README.md). + +### Jetpack Compose + +If you use Jetpack Compose in your application, see Datadog's [dedicated library](integrations/dd-sdk-android-compose/README.md). + +### SQLDelight + +If you use SQLDelight in your application, see Datadog's [dedicated library](integrations/dd-sdk-android-sqldelight/README.md). + +### RxJava + +If you use RxJava in your application, see Datadog's [dedicated library](integrations/dd-sdk-android-rx/README.md). ### Picasso -If you use Picasso, let it use your `OkHttpClient`, and you'll get RUM and APM information about network requests made by Picasso. +If you use Picasso, use it with the `OkHttpClient` that's been instrumented with the Datadog SDK for RUM and APM information about network requests made by Picasso. ```kotlin val picasso = Picasso.Builder(context) @@ -46,7 +58,7 @@ If you use Picasso, let it use your `OkHttpClient`, and you'll get RUM and APM i ### Retrofit -If you use Retrofit, let it use your `OkHttpClient`, and you'll get RUM and APM information about network requests made with Retrofit. +If you use Retrofit, use it with the `OkHttpClient` that's been instrumented with the Datadog SDK for RUM and APM information about network requests made with Retrofit. ```kotlin val retrofitClient = Retrofit.Builder() @@ -57,7 +69,7 @@ If you use Retrofit, let it use your `OkHttpClient`, and you'll get RUM and APM ### Apollo (GraphQL) -If you use Apollo, let it use your `OkHttpClient`, and you'll get RUM and APM information about all the queries performed through Apollo client. +If you use Apollo, use it with the `OkHttpClient` that's been instrumented with the Datadog SDK for RUM and APM information about all the queries performed through Apollo client. ```kotlin val apolloClient = ApolloClient.builder() @@ -66,29 +78,37 @@ If you use Apollo, let it use your `OkHttpClient`, and you'll get RUM and APM in .build() ``` +### Kotlin Coroutines + +If you use Kotlin Coroutines, see Datadog's [dedicated library with extensions for RUM](integrations/dd-sdk-android-rum-coroutines/README.md) and with [extensions for Trace](integrations/dd-sdk-android-trace-coroutines/README.md) + ## Looking up your logs -When you open your console in Datadog, navigate to the Logs section. In the search bar, type `source:android`. This filters your logs to only show the ones coming from mobile applications (Android and iOS). +When you open your console in Datadog, navigate to the [Log Explorer][3]. In the search bar, type `source:android`. This filters your logs to only show the ones coming from Android or Android TV applications. -![Datadog Mobile Logs](docs/images/screenshot.png) +![Datadog Mobile Logs](docs/images/screenshot_logs.png) -## Looking up your logs +## Looking up your spans + +When you open your console in Datadog, navigate to [**APM** > **Services**][4]. In the list of services, you can see all your Android and Android TV applications (by default, the service name matches your application's package name, for example: `com.example.android`). You can access all the traces started from your application. + +![Datadog Mobile Logs](docs/images/screenshot_apm.png) -When you open your console in Datadog, navigate to the Logs section. In the search bar, type `source:android`. This filters your logs to only show the ones coming from mobile applications (Android and iOS). +## Looking up your RUM events -![Datadog Mobile Logs](docs/images/screenshot.png) +When you open your console in Datadog, navigate to the [RUM Explorer][5]. In the side bar, you can select your application and explore Sessions, Views, Actions, Errors, Resources, and Long Tasks. + +![Datadog Mobile Logs](docs/images/screenshot_rum.png) ## Troubleshooting -If you encounter any issue when using the Datadog SDK for Android, please take a look at -the [troubleshooting checklist](docs/TROUBLESHOOTING.md), or at +If you encounter any issue when using the Datadog SDK for Android and Android TV, please take a look at +the [troubleshooting checklist][6], [common problems](docs/advanced_troubleshooting.md), or at the existing [issues](https://github.com/DataDog/dd-sdk-android/issues?q=is%3Aissue). -## Warning - -We have not tested the SDK on Roku devices running with Android OS and we cannot guarantee that it will perform well. -If you encounter any problems while using our SDK for these particular devices please contact us at [Datadog Support](https://docs.datadoghq.com/help/) -or you can directly open an issue in our GitHub project. +
+Datadog cannot guarantee the Android and Android TV SDK's performance on Roku devices running with Android OS. If you encounter any issues when using the SDK for these devices, contact Datadog Support or open an issue in our GitHub project. +
## Contributing @@ -97,3 +117,10 @@ Pull requests are welcome. First, open an issue to discuss what you would like t ## License [Apache License, v2.0](LICENSE) + +[1]: https://docs.datadoghq.com/logs/log_collection/android/?tab=kotlin +[2]: https://docs.datadoghq.com/real_user_monitoring/android/?tab=kotlin +[3]: https://app.datadoghq.com/logs +[4]: https://app.datadoghq.com/apm/services +[5]: https://app.datadoghq.com/rum/explorer +[6]: https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/troubleshooting/android/ \ No newline at end of file diff --git a/ZEN.md b/ZEN.md index 3bdfef143c..c4cf18e433 100644 --- a/ZEN.md +++ b/ZEN.md @@ -32,7 +32,7 @@ This SDK lives in our customer’s applications, and is run on end users devices ## Compatibility - Support old versions of the OS’s - - Android: KitKat (5 years old) + - Android: Lollipop (released in 2014) - Support all main languages; especially the behavior should be the same for any language, but can be enhanced for modern languages. - Android: Java/Kotlin - Support vanilla flavors of the OS first, and add possible extensions for derived flavors of the OSs (Watch, TV, …) diff --git a/build.gradle.kts b/build.gradle.kts index edd45c4b44..aec271aaad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,25 +3,38 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ +@file:Suppress("StringLiteralDuplication") + +import com.android.build.gradle.LibraryExtension +import com.datadog.gradle.config.AndroidConfig +import com.datadog.gradle.config.registerSubModuleAggregationTask +import org.gradle.api.internal.file.UnionFileTree +import org.gradle.api.internal.tasks.DefaultTaskDependencyFactory +import java.util.Properties + +plugins { + `maven-publish` + alias(libs.plugins.nexusPublishGradlePlugin) +} + +version = AndroidConfig.VERSION.name buildscript { repositories { google() mavenCentral() maven { setUrl(com.datadog.gradle.Dependencies.Repositories.Gradle) } - jcenter() } dependencies { - classpath(com.datadog.gradle.Dependencies.ClassPaths.AndroidTools) - classpath(com.datadog.gradle.Dependencies.ClassPaths.AndroidBenchmark) - classpath(com.datadog.gradle.Dependencies.ClassPaths.Kotlin) - classpath(com.datadog.gradle.Dependencies.ClassPaths.KtLint) - classpath(com.datadog.gradle.Dependencies.ClassPaths.Dokka) - classpath(com.datadog.gradle.Dependencies.ClassPaths.Bintray) - classpath(com.datadog.gradle.Dependencies.ClassPaths.Unmock) - classpath(com.datadog.gradle.Dependencies.ClassPaths.Realm) - classpath(com.datadog.gradle.Dependencies.ClassPaths.SQLDelight) + classpath(libs.androidToolsGradlePlugin) + classpath(libs.kotlinGradlePlugin) + classpath(libs.kotlinSPGradlePlugin) + classpath(libs.dokkaGradlePlugin) + classpath(libs.unmockGradlePlugin) + classpath(libs.sqlDelightGradlePlugin) + classpath(libs.binaryCompatibilityGradlePlugin) + classpath(libs.kotlinxSerializationPlugin) } } @@ -30,83 +43,81 @@ allprojects { google() mavenCentral() maven { setUrl(com.datadog.gradle.Dependencies.Repositories.Jitpack) } - jcenter() - flatDir { dirs("libs") } + } +} + +nexusPublishing { + this.repositories { + sonatype { + stagingProfileId = "378eecbbe2cf9" + val sonatypeUsername = System.getenv("CENTRAL_PUBLISHER_USERNAME") + val sonatypePassword = System.getenv("CENTRAL_PUBLISHER_PASSWORD") + if (sonatypeUsername != null) username.set(sonatypeUsername) + if (sonatypePassword != null) password.set(sonatypePassword) + // see https://github.com/gradle-nexus/publish-plugin#publishing-to-maven-central-via-sonatype-central + // For official documentation: + // staging repo publishing https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration + // snapshot publishing https://central.sonatype.org/publish/publish-portal-snapshots/#publishing-via-other-methods + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) + } } } task("clean") { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } tasks.register("checkAll") { dependsOn( - "ktlintCheckAll", - "detektAll", "lintCheckAll", "unitTestAll", - "jacocoReportAll", "instrumentTestAll" ) } -tasks.register("assembleAll") { - dependsOn( - ":dd-sdk-android:assemble", - ":dd-sdk-android-coil:assemble", - ":dd-sdk-android-fresco:assemble", - ":dd-sdk-android-glide:assemble", - ":dd-sdk-android-ktx:assemble", - ":dd-sdk-android-ndk:assemble", - ":dd-sdk-android-rx:assemble", - ":dd-sdk-android-sqldelight:assemble", - ":dd-sdk-android-timber:assemble" - ) -} +registerSubModuleAggregationTask("assembleLibraries", "assemble") +registerSubModuleAggregationTask("assembleLibrariesDebug", "assembleDebug") +registerSubModuleAggregationTask("assembleLibrariesRelease", "assembleRelease") + +registerSubModuleAggregationTask("unitTestRelease", "testReleaseUnitTest") +registerSubModuleAggregationTask( + "unitTestReleaseFeatures", + "testReleaseUnitTest", + ":features:" +) +registerSubModuleAggregationTask("unitTestReleaseIntegrations", "testReleaseUnitTest", ":integrations:") -tasks.register("unitTestRelease") { +registerSubModuleAggregationTask("unitTestDebug", "testDebugUnitTest") +registerSubModuleAggregationTask( + "unitTestDebugFeatures", + "testDebugUnitTest", + ":features:" +) +registerSubModuleAggregationTask("unitTestDebugIntegrations", "testDebugUnitTest", ":integrations:") +tasks.register("unitTestDebugSamples") { dependsOn( - ":dd-sdk-android:testReleaseUnitTest", - ":dd-sdk-android-coil:testReleaseUnitTest", - ":dd-sdk-android-fresco:testReleaseUnitTest", - ":dd-sdk-android-glide:testReleaseUnitTest", - ":dd-sdk-android-ktx:testReleaseUnitTest", - ":dd-sdk-android-ndk:testReleaseUnitTest", - ":dd-sdk-android-rx:testReleaseUnitTest", - ":dd-sdk-android-sqldelight:testReleaseUnitTest", - ":dd-sdk-android-timber:testReleaseUnitTest" + ":sample:benchmark:testDebugUnitTest" ) } -tasks.register("unitTestDebug") { +tasks.register("assembleSampleRelease") { dependsOn( - ":dd-sdk-android:testDebugUnitTest", - ":dd-sdk-android:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-coil:testDebugUnitTest", - ":dd-sdk-android-coil:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-fresco:testDebugUnitTest", - ":dd-sdk-android-fresco:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-glide:testDebugUnitTest", - ":dd-sdk-android-glide:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-ktx:testDebugUnitTest", - ":dd-sdk-android-ktx:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-ndk:testDebugUnitTest", - ":dd-sdk-android-ndk:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-rx:testDebugUnitTest", - ":dd-sdk-android-rx:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-sqldelight:testDebugUnitTest", - ":dd-sdk-android-sqldelight:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-timber:testDebugUnitTest", - ":dd-sdk-android-timber:jacocoTestDebugUnitTestReport" + ":sample:kotlin:assembleUs1Release", + ":sample:wear:assembleUs1Release", + ":sample:vendor-lib:assembleRelease", + ":sample:automotive:assembleRelease", + ":sample:tv:assembleRelease" ) } tasks.register("unitTestTools") { dependsOn( - ":sample:java:assembleRelease", - ":sample:kotlin:assembleRelease", + ":tools:unit:testJvmReleaseUnitTest", ":tools:detekt:test", - ":tools:unit:testReleaseUnitTest" + ":tools:lint:test", + ":tools:noopfactory:test", + ":tools:benchmark:test" ) } @@ -118,84 +129,34 @@ tasks.register("unitTestAll") { ) } -tasks.register("ktlintCheckAll") { - dependsOn( - ":dd-sdk-android:ktlintCheck", - ":dd-sdk-android-coil:ktlintCheck", - ":dd-sdk-android-fresco:ktlintCheck", - ":dd-sdk-android-glide:ktlintCheck", - ":dd-sdk-android-ktx:ktlintCheck", - ":dd-sdk-android-ndk:ktlintCheck", - ":dd-sdk-android-rx:ktlintCheck", - ":dd-sdk-android-sqldelight:ktlintCheck", - ":dd-sdk-android-timber:ktlintCheck", - ":instrumented:integration:ktlintCheck", - ":instrumented:benchmark:ktlintCheck", - ":tools:detekt:ktlintCheck", - ":tools:unit:ktlintCheck" - ) +registerSubModuleAggregationTask("lintCheckAll", "lintRelease") { + dependsOn(":tools:lint:lint") } +registerSubModuleAggregationTask("checkDependencyLicencesAll", "checkDependencyLicenses") -tasks.register("lintCheckAll") { - dependsOn( - ":dd-sdk-android:lintRelease", - ":dd-sdk-android-coil:lintRelease", - ":dd-sdk-android-fresco:lintRelease", - ":dd-sdk-android-glide:lintRelease", - ":dd-sdk-android-ktx:lintRelease", - ":dd-sdk-android-ndk:lintRelease", - ":dd-sdk-android-rx:lintRelease", - ":dd-sdk-android-sqldelight:lintRelease", - ":dd-sdk-android-timber:lintRelease" - ) -} +registerSubModuleAggregationTask("checkApiSurfaceChangesAll", "checkApiSurfaceChanges") -tasks.register("detektAll") { - dependsOn( - ":dd-sdk-android:detekt", - ":dd-sdk-android-coil:detekt", - ":dd-sdk-android-fresco:detekt", - ":dd-sdk-android-glide:detekt", - ":dd-sdk-android-ktx:detekt", - ":dd-sdk-android-ndk:detekt", - ":dd-sdk-android-rx:detekt", - ":dd-sdk-android-sqldelight:detekt", - ":dd-sdk-android-timber:detekt", - ":instrumented:integration:detekt", - ":instrumented:benchmark:detekt", - ":tools:unit:detekt" - ) -} +registerSubModuleAggregationTask("checkTransitiveDependenciesListAll", "checkTransitiveDependenciesList") -tasks.register("jacocoReportAll") { - dependsOn( - ":dd-sdk-android:jacocoTestDebugUnitTestReport", - ":dd-sdk-android:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-coil:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-coil:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-fresco:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-fresco:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-glide:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-glide:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-ktx:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-ktx:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-ndk:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-ndk:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-rx:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-rx:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-sqldelight:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-sqldelight:jacocoTestReleaseUnitTestReport", - ":dd-sdk-android-timber:jacocoTestDebugUnitTestReport", - ":dd-sdk-android-timber:jacocoTestReleaseUnitTestReport", - ":tools:detekt:jacocoTestReport", - ":tools:unit:jacocoTestDebugUnitTestReport", - ":tools:unit:jacocoTestReleaseUnitTestReport" - ) +/** + * Task necessary to be compliant with the shared Android static analysis pipeline + */ +tasks.register("checkGeneratedFiles") { + dependsOn("checkDependencyLicencesAll") + dependsOn("checkApiSurfaceChangesAll") + dependsOn("checkTransitiveDependenciesListAll") } +registerSubModuleAggregationTask("koverReportAll", "koverXmlReportRelease") +registerSubModuleAggregationTask("koverReportFeatures", "koverXmlReportRelease", ":features:") +registerSubModuleAggregationTask("koverReportIntegrations", "koverXmlReportRelease", ":integrations:") + +registerSubModuleAggregationTask("printDetektClasspathAll", "printDetektClasspath") +registerSubModuleAggregationTask("printDetektClasspathFeatures", "printDetektClasspath", ":features:") +registerSubModuleAggregationTask("printDetektClasspathIntegrations", "printDetektClasspath", ":integrations:") + tasks.register("instrumentTestAll") { dependsOn(":instrumented:integration:connectedCheck") - dependsOn(":instrumented:benchmark:connectedCheck") } tasks.register("buildIntegrationTestsArtifacts") { @@ -204,7 +165,92 @@ tasks.register("buildIntegrationTestsArtifacts") { } tasks.register("buildNdkIntegrationTestsArtifacts") { - dependsOn(":dd-sdk-android-ndk:assembleDebugAndroidTest") + dependsOn(":features:dd-sdk-android-ndk:assembleDebugAndroidTest") // we need this artifact to trick Bitrise dependsOn(":instrumented:integration:assembleDebug") } + +tasks.register("printSdkDebugRuntimeClasspath") { + val fileTreeClassPathCollector = UnionFileTree( + DefaultTaskDependencyFactory.withNoAssociatedProject() + ) + val nonFileTreeClassPathCollector = mutableListOf() + + allprojects.minus(project).forEach { subproject -> + val childTask = subproject.tasks.register("printDebugRuntimeClasspath") { + doLast { + val ext = + subproject.extensions.findByType(LibraryExtension::class.java) ?: return@doLast + val classpath = ext.libraryVariants + .filter { it.name == "jvmDebug" || it.name == "debug" } + .map { libVariant -> + // returns also test part of classpath for now, no idea how to filter it out + libVariant.getCompileClasspath(null).filter { it.exists() } + } + .first() + if (classpath is FileTree) { + fileTreeClassPathCollector.addToUnion(classpath) + } else { + nonFileTreeClassPathCollector += classpath + } + } + } + this@register.dependsOn(childTask) + } + doLast { + val fileCollections = mutableListOf() + fileCollections.addAll(nonFileTreeClassPathCollector) + if (!fileTreeClassPathCollector.isEmpty) { + fileCollections.add(fileTreeClassPathCollector) + } + val result = fileCollections.flatMap { + it.files + }.toMutableSet() + + val localPropertiesFile = File(project.rootDir, "local.properties") + if (localPropertiesFile.exists()) { + val localProperties = Properties().apply { + localPropertiesFile.inputStream().use { load(it) } + } + val sdkDirPath = localProperties["sdk.dir"] + val androidJarFilePath = listOf( + sdkDirPath, + "platforms", + "android-${AndroidConfig.TARGET_SDK}", + "android.jar" + ) + result += File(androidJarFilePath.joinToString(File.separator)) + } + + val envSdkHome = System.getenv("ANDROID_SDK_ROOT") + if (!envSdkHome.isNullOrBlank()) { + val androidJarFilePath = listOf( + envSdkHome, + "platforms", + "android-${AndroidConfig.TARGET_SDK}", + "android.jar" + ) + result += File(androidJarFilePath.joinToString(File.separator)) + } + + File("sdk_classpath").writeText(result.joinToString(File.pathSeparator) { it.absolutePath }) + } +} + +tasks.register("listAllPublishedArtifactIds") { + doLast { + val artifactIds = rootProject.subprojects.flatMap { subproject -> + val publishing = subproject.extensions.findByType() + publishing?.publications?.mapNotNull { publication -> + if (publication is MavenPublication) { + publication.artifactId + } else { + null + } + }.orEmpty() + } + artifactIds.forEach { + println(it) + } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index f9939059f3..e38008f94d 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,28 +1,23 @@ /* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.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 2016-Present Datadog, Inc. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `kotlin-dsl` - id("java-gradle-plugin") - id("com.github.ben-manes.versions") version ("0.27.0") + alias(libs.plugins.versionsGradlePlugin) } buildscript { - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61") - } repositories { mavenCentral() } } -apply(plugin = "kotlin") - repositories { mavenCentral() google() @@ -34,45 +29,38 @@ repositories { dependencies { // Dependencies used to configure the gradle plugins - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61") - implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.1.1") - implementation("org.jlleitschuh.gradle:ktlint-gradle:9.4.0") - implementation("com.android.tools.build:gradle:4.0.1") - implementation("com.github.ben-manes:gradle-versions-plugin:0.27.0") - implementation("me.xdrop:fuzzywuzzy:1.2.0") - implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.4.10") - implementation("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4") + implementation(libs.kotlinGradlePlugin) + implementation(libs.androidToolsGradlePlugin) + implementation(libs.versionsGradlePlugin) + implementation(libs.dokkaGradlePlugin) + implementation(libs.dependencyLicenseGradlePlugin) + implementation(libs.kover) // check api surface - implementation("com.github.kotlinx.ast:grammar-kotlin-parser-antlr-kotlin-jvm:c35b50fa44") + implementation(libs.kotlinGrammarParser) + implementation(libs.kotlinAntlrRuntime) // JsonSchema 2 Poko - implementation("com.google.code.gson:gson:2.8.6") - implementation("com.squareup:kotlinpoet:1.6.0") + implementation(libs.gson) + implementation(libs.kotlinPoet) + + // Verification Metadata XML + implementation(libs.kotlinXmlBuilder) // Tests - testImplementation("junit:junit:4.12") - testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") - testImplementation("net.wuerl.kotlin:assertj-core-kotlin:0.2.1") - testImplementation("com.github.xgouchet.Elmyr:core:1.0.0") - testImplementation("com.github.xgouchet.Elmyr:inject:1.0.0") - testImplementation("com.github.xgouchet.Elmyr:junit4:1.0.0") - testImplementation("com.github.xgouchet.Elmyr:jvm:1.0.0") + testImplementation(libs.jUnit4) + testImplementation(libs.mockitoKotlin) + testImplementation(libs.assertJ) + testImplementation(libs.elmyr) + testImplementation(libs.elmyrInject) + testImplementation(libs.elmyrJUnit4) + testImplementation(libs.elmyrJVM) // Json Schema validation - testImplementation("com.github.everit-org.json-schema:org.everit.json.schema:1.12.1") + testImplementation(libs.jsonSchemaValidator) } gradlePlugin { plugins { - register("reviewBenchmark") { - id = "reviewBenchmark" // the alias - implementationClass = "com.datadog.gradle.plugin.benchmark.ReviewBenchmarkPlugin" - } - register("thirdPartyLicences") { - id = "thirdPartyLicences" // the alias - implementationClass = "com.datadog.gradle.plugin.checklicenses.ThirdPartyLicensesPlugin" - } register("apiSurface") { id = "apiSurface" // the alias implementationClass = "com.datadog.gradle.plugin.apisurface.ApiSurfacePlugin" @@ -81,33 +69,42 @@ gradlePlugin { id = "cloneDependencies" // the alias implementationClass = "com.datadog.gradle.plugin.gitclone.GitCloneDependenciesPlugin" } - register("jsonschema2poko") { - id = "jsonschema2poko" // the alias - implementationClass = "com.datadog.gradle.plugin.jsonschema.JsonSchemaPlugin" - } register("transitiveDependencies") { id = "transitiveDependencies" // the alias implementationClass = "com.datadog.gradle.plugin.transdeps.TransitiveDependenciesPlugin" } + register("verificationXml") { + id = "verificationXml" // the alias + implementationClass = "com.datadog.gradle.plugin.verification.VerificationXmlPlugin" + } } } -tasks.withType { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +java.targetCompatibility = JavaVersion.VERSION_17 +java.sourceCompatibility = JavaVersion.VERSION_17 + +tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } } tasks { - register("copyTestRes") { + val copyTestRes = register("copyTestRes") { from("$projectDir/src/test/kotlin/com/example/model") into("$projectDir/src/test/resources/output") } - register("deleteTestRes") { + val deleteTestRes = register("deleteTestRes") { delete("$projectDir/src/test/resources/output/") } -} -tasks.named("test") { - dependsOn("copyTestRes") - finalizedBy("deleteTestRes") + named("processTestResources") { + dependsOn(copyTestRes) + } + + named("test") { + dependsOn(copyTestRes) + finalizedBy(deleteTestRes) + } } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000000..f0a5ee0a1b --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/Dependencies.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/Dependencies.kt index 865160109c..5566fad91b 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/Dependencies.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/Dependencies.kt @@ -9,199 +9,10 @@ package com.datadog.gradle object Dependencies { object Versions { - // Commons - const val Kotlin = "1.4.0" - const val Gson = "2.8.6" - const val OkHttp = "3.12.6" - const val KronosNTP = "0.0.1-alpha10" - - // Android - const val AndroidToolsPlugin = "4.0.1" - const val AndroidXAnnotations = "1.1.0" - const val AndroidXAppCompat = "1.2.0" - const val AndroidXCore = "1.3.1" - const val AndroidXMultidex = "2.0.1" - const val AndroidXNavigation = "2.3.0" - const val AndroidXRecyclerView = "1.1.0" - const val AndroidXWorkManager = "2.4.0" - - // DD-TRACE-OT - const val OpenTracing = "0.32.0" - - // JUnit - const val JUnitJupiter = "5.6.2" - const val JUnitPlatform = "1.6.2" - const val JUnitVintage = "5.6.2" - const val JunitMockitoExt = "3.5.13" - - // Tests Tools - const val AssertJ = "0.2.1" - const val Elmyr = "1.2.0" - const val Jacoco = "0.8.4" - const val MockitoKotlin = "2.2.0" - const val JetpackBenchmark = "1.0.0" - - // Tools - const val Detekt = "1.6.0" - const val KtLint = "9.4.0" - const val Dokka = "1.4.10" - const val Bintray = "1.8.4" - const val Unmock = "0.7.5" - const val Robolectric = "4.4_r1-robolectric-r2" // Use lowest API - - // AndroidJunit - const val AndroidJunitRunner = "1.3.0" - const val AndroidExtJunit = "1.1.2" - const val AndroidJunitCore = "1.3.0" - const val Espresso = "3.3.0" - - // Sample Apps - const val ConstraintLayout = "2.0.1" - const val GoogleMaterial = "1.0.0" - - // Integrations - const val Coil = "1.0.0" - const val Fresco = "2.3.0" - const val Glide = "4.11.0" - const val Picasso = "2.8" - const val Realm = "6.0.2" - const val Room = "2.2.5" - const val RxJava = "3.0.0" - const val SQLDelight = "1.4.3" - const val Timber = "4.7.1" - const val Coroutines = "1.3.9" // NDK - const val NdkVersion = "21.3.6528147" - const val CMakeVersion = "3.10.2" - } - - object Libraries { - - @JvmField - val OpenTracing = arrayOf( - "io.opentracing:opentracing-api:${Versions.OpenTracing}", - "io.opentracing:opentracing-noop:${Versions.OpenTracing}", - "io.opentracing:opentracing-util:${Versions.OpenTracing}" - ) - const val Kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.Kotlin}" - const val KotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.Kotlin}" - - const val Gson = "com.google.code.gson:gson:${Versions.Gson}" - const val AssertJ = "net.wuerl.kotlin:assertj-core-kotlin:${Versions.AssertJ}" - - const val OkHttp = "com.squareup.okhttp3:okhttp:${Versions.OkHttp}" - const val KronosNTP = "com.lyft.kronos:kronos-android:${Versions.KronosNTP}" - - const val AndroidXMultidex = "androidx.multidex:multidex:${Versions.AndroidXMultidex}" - - val JetpackBenchmark = arrayOf( - "androidx.benchmark:benchmark-junit4:${Versions.JetpackBenchmark}", - "androidx.test.ext:junit:1.1.1" - ) - - const val AndroidXAnnotation = - "androidx.annotation:annotation:${Versions.AndroidXAnnotations}" - const val AndroidXAppCompat = "androidx.appcompat:appcompat:${Versions.AndroidXAppCompat}" - const val AndroidXCore = "androidx.core:core:${Versions.AndroidXCore}" - val AndroidXNavigation = arrayOf( - "androidx.navigation:navigation-fragment:${Versions.AndroidXNavigation}", - "androidx.navigation:navigation-runtime-ktx:${Versions.AndroidXNavigation}" - ) - const val AndroidXRecyclerView = - "androidx.recyclerview:recyclerview:${Versions.AndroidXRecyclerView}" - const val AndroidXWorkManager = "androidx.work:work-runtime:${Versions.AndroidXWorkManager}" - - @JvmField - val JUnit5 = arrayOf( - "org.junit.platform:junit-platform-launcher:${Versions.JUnitPlatform}", - "org.junit.vintage:junit-vintage-engine:${Versions.JUnitVintage}", - "org.junit.jupiter:junit-jupiter:${Versions.JUnitJupiter}", - "org.mockito:mockito-junit-jupiter:${Versions.JunitMockitoExt}" - ) - - @JvmField - val TestTools = arrayOf( - AssertJ, - "com.github.xgouchet.Elmyr:core:${Versions.Elmyr}", - "com.github.xgouchet.Elmyr:inject:${Versions.Elmyr}", - "com.github.xgouchet.Elmyr:junit5:${Versions.Elmyr}", - "com.github.xgouchet.Elmyr:jvm:${Versions.Elmyr}", - "com.nhaarman.mockitokotlin2:mockito-kotlin:${Versions.MockitoKotlin}" - ) - - const val Elmyr = "com.github.xgouchet.Elmyr:core:${Versions.Elmyr}" - - @JvmField - val IntegrationTests = arrayOf( - // Core library - "androidx.test:core:${Versions.AndroidJunitCore}", - // AndroidJUnitRunner and JUnit Rules - "androidx.test:runner:${Versions.AndroidJunitRunner}", - "androidx.test:runner:${Versions.AndroidJunitRunner}", - "androidx.test:rules:${Versions.AndroidJunitRunner}", - "androidx.test.ext:junit:${Versions.AndroidExtJunit}", - // Espresso - "androidx.test.espresso:espresso-core:${Versions.Espresso}", - "androidx.test.espresso:espresso-contrib:${Versions.Espresso}", - "androidx.test.espresso:espresso-intents:${Versions.Espresso}", - // Elmyr - "com.github.xgouchet.Elmyr:core:${Versions.Elmyr}", - "com.github.xgouchet.Elmyr:inject:${Versions.Elmyr}", - "com.github.xgouchet.Elmyr:junit4:${Versions.Elmyr}" - ) - - @JvmField - val AndroidxSupportBase = arrayOf( - AndroidXAppCompat, - "androidx.constraintlayout:constraintlayout:${Versions.ConstraintLayout}", - "com.google.android.material:material:${Versions.GoogleMaterial}" - ) - - // Integrations - - const val Coil = "io.coil-kt:coil:${Versions.Coil}" - val Fresco = arrayOf( - "com.facebook.fresco:fresco:${Versions.Fresco}", - "com.facebook.fresco:imagepipeline-okhttp3:${Versions.Fresco}" - ) - val Glide = arrayOf( - "com.github.bumptech.glide:annotations:${Versions.Glide}", - "com.github.bumptech.glide:glide:${Versions.Glide}", - "com.github.bumptech.glide:okhttp3-integration:${Versions.Glide}" - ) - const val Picasso = "com.squareup.picasso:picasso:${Versions.Picasso}" - - const val Room = "androidx.room:room-runtime:${Versions.Room}" - const val RxJava = "io.reactivex.rxjava3:rxjava:${Versions.RxJava}" - const val SQLDelight = "com.squareup.sqldelight:android-driver:${Versions.SQLDelight}" - const val Timber = "com.jakewharton.timber:timber:${Versions.Timber}" - - val Coroutines = arrayOf( - "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.Coroutines}", - "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.Coroutines}" - ) - - // Tools - - const val DetektCli = "io.gitlab.arturbosch.detekt:detekt-cli:${Versions.Detekt}" - const val DetektApi = "io.gitlab.arturbosch.detekt:detekt-api:${Versions.Detekt}" - const val DetektTest = "io.gitlab.arturbosch.detekt:detekt-test:${Versions.Detekt}" - const val OkHttpMock = "com.squareup.okhttp3:mockwebserver:${Versions.OkHttp}" - const val Robolectric = "org.robolectric:android-all:${Versions.Robolectric}" - } - - object ClassPaths { - const val AndroidTools = "com.android.tools.build:gradle:${Versions.AndroidToolsPlugin}" - const val Kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin}" - const val KtLint = "org.jlleitschuh.gradle:ktlint-gradle:${Versions.KtLint}" - const val AndroidBenchmark = - "androidx.benchmark:benchmark-gradle-plugin:${Versions.JetpackBenchmark}" - const val Dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${Versions.Dokka}" - const val Bintray = "com.jfrog.bintray.gradle:gradle-bintray-plugin:${Versions.Bintray}" - const val Unmock = "de.mobilej.unmock:UnMockPlugin:${Versions.Unmock}" - const val Realm = "io.realm:realm-gradle-plugin:${Versions.Realm}" - const val SQLDelight = "com.squareup.sqldelight:gradle-plugin:${Versions.SQLDelight}" + const val Ndk = "28.0.13004108" + const val CMake = "3.22.1" } object Repositories { @@ -209,9 +20,4 @@ object Dependencies { const val Google = "https://maven.google.com/" const val Jitpack = "https://jitpack.io/" } - - object AnnotationProcessors { - const val Glide = "com.github.bumptech.glide:compiler:${Versions.Glide}" - const val Room = "androidx.room:room-compiler:${Versions.Room}" - } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/Properties.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/Properties.kt new file mode 100644 index 0000000000..be69b7b940 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/Properties.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle + +object Properties { + // needed to bring some classes into classpath which are missing on the lower APIs + const val USE_API21_JAVA_BACKPORT = "use-api21-java-backport" + const val USE_DESUGARING = "use-desugaring" +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt index 689222b390..c35e27c02e 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt @@ -6,13 +6,89 @@ package com.datadog.gradle.config +import com.android.build.api.dsl.CompileOptions +import com.android.build.gradle.LibraryExtension +import com.datadog.gradle.plugin.licenses.DependencyLicensesExtension import com.datadog.gradle.utils.Version +import org.gradle.api.JavaVersion +import org.gradle.api.Project object AndroidConfig { - const val TARGET_SDK = 30 - const val MIN_SDK = 19 - const val BUILD_TOOLS_VERSION = "30.0.2" + const val TARGET_SDK = 36 + const val MIN_SDK = 23 + const val MIN_SDK_FOR_AUTO = 29 + const val BUILD_TOOLS_VERSION = "36.0.0" - val VERSION = Version(1, 7, 0, Version.Type.Alpha(1)) + val VERSION = Version(3, 2, 0, Version.Type.Release) +} + +// TODO RUM-628 Switch to Java 17 bytecode +fun CompileOptions.java11() { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +fun CompileOptions.java17() { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +fun Project.androidLibraryConfig() { + extensionConfig { + compileSdk = AndroidConfig.TARGET_SDK + buildToolsVersion = AndroidConfig.BUILD_TOOLS_VERSION + + defaultConfig { + minSdk = AndroidConfig.MIN_SDK + } + + compileOptions { + java11() + } + + sourceSets.all { + java.srcDir("src/$name/kotlin") + } + sourceSets.named("main") { + java.srcDir("build/generated/json2kotlin/main/kotlin") + } + libraryVariants.configureEach { + addJavaSourceFoldersToModel( + layout.buildDirectory.dir("generated/ksp/$name/kotlin").get().asFile + ) + } + + @Suppress("UnstableApiUsage") + testOptions { + unitTests.isReturnDefaultValues = true + } + + lint { + warningsAsErrors = true + abortOnError = true + checkReleaseBuilds = false + checkGeneratedSources = true + ignoreTestSources = true + disable.addAll( + listOf( + "UseKtx" // https://googlesamples.github.io/android-custom-lint-rules/checks/UseKtx.md.html + ) + ) + } + + packaging { + resources { + excludes += listOf( + "META-INF/jvm.kotlin_module", + "META-INF/LICENSE.md", + "META-INF/LICENSE-notice.md" + ) + } + } + } + + extensionConfig { + transitiveDependencies = true + } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/BaseExtensionConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/BaseExtensionConfig.kt index 740f5a8b3d..d67a2ae5a7 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/BaseExtensionConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/BaseExtensionConfig.kt @@ -13,17 +13,14 @@ import org.gradle.kotlin.dsl.findByType inline fun Project.extensionConfig( crossinline configure: T.() -> Unit ) { - - project.afterEvaluate { - val ext: T? = extensions.findByType(T::class) - ext?.configure() - } + val ext: T? = extensions.findByType(T::class) + ext?.configure() } inline fun Project.taskConfig( crossinline configure: T.() -> Unit ) { project.afterEvaluate { - tasks.withType(T::class.java) { configure() } + tasks.withType(T::class.java).configureEach { configure() } } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/DependencyUpdateConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/DependencyUpdateConfig.kt index 6fcb322a4b..70d04f17fd 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/DependencyUpdateConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/DependencyUpdateConfig.kt @@ -10,7 +10,6 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask import org.gradle.api.Project fun Project.dependencyUpdateConfig() { - taskConfig { revision = "release" } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektConfig.kt deleted file mode 100644 index c503713da0..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektConfig.kt +++ /dev/null @@ -1,37 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.config - -import io.gitlab.arturbosch.detekt.Detekt -import io.gitlab.arturbosch.detekt.extensions.DetektExtension -import org.gradle.api.Project -import org.gradle.kotlin.dsl.withType - -fun Project.detektConfig(excludes: List = emptyList()) { - - extensionConfig { - input = files("$projectDir/src/main/kotlin") - config = files("${project.rootDir}/detekt.yml") - - reports { - xml { - enabled = true - destination = file("build/reports/detekt.xml") - } - } - } - - tasks.withType { - dependsOn(":tools:detekt:assemble") - - setExcludes(excludes) - } - - tasks.named("check") { - dependsOn("detekt") - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektCustomConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektCustomConfig.kt new file mode 100644 index 0000000000..763b93d856 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektCustomConfig.kt @@ -0,0 +1,136 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.config + +import com.android.build.gradle.LibraryExtension +import org.gradle.api.Project +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileTree +import org.gradle.api.internal.file.UnionFileTree +import org.gradle.api.internal.tasks.DefaultTaskDependencyFactory +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.JavaExec +import java.io.File +import java.util.Properties + +fun Project.detektCustomConfig() { + val ext = extensions.findByType(LibraryExtension::class.java) + + tasks.register("printDetektClasspath") { + group = "datadog" + + doLast { + val fileTreeClassPathCollector = UnionFileTree( + DefaultTaskDependencyFactory.withNoAssociatedProject() + ) + val nonFileTreeClassPathCollector = mutableListOf() + + val classpath = ext?.libraryVariants.orEmpty() + .filter { it.name == "jvmDebug" || it.name == "debug" } + .map { libVariant -> + // returns also test part of classpath for now, no idea how to filter it out + libVariant.getCompileClasspath(null).filter { it.exists() } + } + .firstOrNull() + + if (classpath is FileTree) { + fileTreeClassPathCollector.addToUnion(classpath) + } else if (classpath != null) { + nonFileTreeClassPathCollector += classpath + } + + val fileCollections = mutableListOf() + fileCollections.addAll(nonFileTreeClassPathCollector) + if (!fileTreeClassPathCollector.isEmpty) { + fileCollections.add(fileTreeClassPathCollector) + } + val result = fileCollections.flatMap { + it.files + }.toMutableSet() + val localPropertiesFile = File(project.rootDir, "local.properties") + if (localPropertiesFile.exists()) { + val localProperties = Properties().apply { + localPropertiesFile.inputStream().use { load(it) } + } + val sdkDirPath = localProperties["sdk.dir"] + val androidJarFilePath = listOf( + sdkDirPath, + "platforms", + "android-${AndroidConfig.TARGET_SDK}", + "android.jar" + ) + result += File(androidJarFilePath.joinToString(File.separator)) + } + val envSdkHome = System.getenv("ANDROID_SDK_ROOT") + if (!envSdkHome.isNullOrBlank()) { + val androidJarFilePath = listOf( + envSdkHome, + "platforms", + "android-${AndroidConfig.TARGET_SDK}", + "android.jar" + ) + result += File(androidJarFilePath.joinToString(File.separator)) + } + + val output = result.joinToString(File.pathSeparator) { it.absolutePath } + File(projectDir, "detekt_classpath").writeText(output) + } + } + + tasks.register("unzipAarForDetekt", Copy::class.java) { + from(zipTree(layout.buildDirectory.file("outputs/aar/${project.name}-release.aar"))) + into(layout.buildDirectory.dir("extracted")) + } + + tasks.register("customDetektRules", JavaExec::class.java) { + group = "datadog" + + classpath = files("${rootDir.absolutePath}/detekt-cli-1.23.4-all.jar") + + args( + "--config", + "${rootDir.absolutePath}/detekt_custom_general.yml," + + "${rootDir.absolutePath}/detekt_custom_safe_calls.yml," + + "${rootDir.absolutePath}/detekt_custom_unsafe_calls.yml" + ) + args("--plugins", "${rootDir.absolutePath}/tools/detekt/build/libs/detekt.jar") + args("-i", projectDir.absolutePath) + args("-ex", "**/*.kts") + args("--jvm-target", "11") + + val moduleDependencies = configurations + .filter { it.name == "implementation" || it.name == "api" } + .flatMap { it.dependencies.filterIsInstance() } + .map { it.path } + .toSet() + .let { + // api configurations have canBeResolved=false, so we cannot go inside them to see transitive + // module dependencies, so including common modules + if (project.path == ":dd-sdk-android-internal") { + it + } else if (project.path == ":dd-sdk-android-core") { + it + ":dd-sdk-android-internal" + } else { + it + setOf(":dd-sdk-android-core", ":dd-sdk-android-internal") + } + } + + val externalDependencies = File("${projectDir.absolutePath}/detekt_classpath").readText() + val moduleDependenciesClasses = moduleDependencies.map { + "${rootDir.absolutePath}${it.replace(':', '/')}/build/extracted/classes.jar" + }.joinToString(":") + + val dependencies = if (moduleDependenciesClasses.isBlank()) { + externalDependencies + } else { + "$externalDependencies:$moduleDependenciesClasses" + } + + args("-cp", dependencies) + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/FlavorBuildConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/FlavorBuildConfig.kt index e9c85b58c1..f9cdd4a7ee 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/FlavorBuildConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/FlavorBuildConfig.kt @@ -6,11 +6,14 @@ package com.datadog.gradle.config -import com.android.build.gradle.internal.dsl.ProductFlavor +import com.android.build.api.dsl.ApplicationDefaultConfig +import com.android.build.api.dsl.ApplicationProductFlavor import com.google.gson.Gson +import org.gradle.api.Project import java.io.File +import java.util.Locale -private fun sampleAppConfig(filePath: String): SampleAppConfig { +fun sampleAppConfig(filePath: String): SampleAppConfig { val file = File(filePath) if (!file.exists()) { return SampleAppConfig() @@ -21,10 +24,41 @@ private fun sampleAppConfig(filePath: String): SampleAppConfig { } } -fun configureFlavorForSampleApp(flavor: ProductFlavor, rootDir: File) { - val config = - sampleAppConfig("${rootDir.absolutePath}/config/${flavor.name}.json") - println("Configuring flavor: [${flavor.name}] with config: [$config]") +@Suppress("UnstableApiUsage") +fun ApplicationDefaultConfig.configureFlavorForBenchmark( + rootDir: File +) { + val config = sampleAppConfig("${rootDir.absolutePath}/config/benchmark.json") + buildConfigField( + "String", + "BENCHMARK_RUM_APPLICATION_ID", + "\"${config.rumApplicationId}\"" + ) + buildConfigField( + "String", + "BENCHMARK_CLIENT_TOKEN", + "\"${config.token}\"" + ) + buildConfigField( + "String", + "BENCHMARK_API_KEY", + "\"${config.apiKey}\"" + ) + buildConfigField( + "String", + "BENCHMARK_APPLICATION_KEY", + "\"${config.applicationKey}\"" + ) +} + +@Suppress("UnstableApiUsage") +fun configureFlavorForSampleApp( + project: Project, + flavor: ApplicationProductFlavor, + rootDir: File +) { + val config = sampleAppConfig("${rootDir.absolutePath}/config/${flavor.name}.json") + project.logger.info("Configuring flavor: [${flavor.name}] with config: [$config]") flavor.buildConfigField( "String", "DD_OVERRIDE_LOGS_URL", @@ -40,6 +74,11 @@ fun configureFlavorForSampleApp(flavor: ProductFlavor, rootDir: File) { "DD_OVERRIDE_RUM_URL", "\"${config.rumEndpoint}\"" ) + flavor.buildConfigField( + "String", + "DD_OVERRIDE_SESSION_REPLAY_URL", + "\"${config.sessionReplayEndpoint}\"" + ) flavor.buildConfigField( "String", "DD_RUM_APPLICATION_ID", @@ -60,4 +99,43 @@ fun configureFlavorForSampleApp(flavor: ProductFlavor, rootDir: File) { "DD_APPLICATION_KEY", "\"${config.applicationKey}\"" ) + flavor.buildConfigField( + "String", + "DD_SITE_NAME", + "\"${flavor.name.uppercase(Locale.US)}\"" + ) +} + +@Suppress("UnstableApiUsage") +fun ApplicationDefaultConfig.configureFlavorForTvApp( + rootDir: File +) { + val config = sampleAppConfig("${rootDir.absolutePath}/config/tv.json") + buildConfigField( + "String", + "DD_RUM_APPLICATION_ID", + "\"${config.rumApplicationId}\"" + ) + buildConfigField( + "String", + "DD_CLIENT_TOKEN", + "\"${config.token}\"" + ) +} + +@Suppress("UnstableApiUsage") +fun ApplicationDefaultConfig.configureFlavorForAutoApp( + rootDir: File +) { + val config = sampleAppConfig("${rootDir.absolutePath}/config/auto.json") + buildConfigField( + "String", + "DD_RUM_APPLICATION_ID", + "\"${config.rumApplicationId}\"" + ) + buildConfigField( + "String", + "DD_CLIENT_TOKEN", + "\"${config.token}\"" + ) } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/JUnitConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/JUnitConfig.kt index d83d0f8241..6188df1909 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/JUnitConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/JUnitConfig.kt @@ -9,15 +9,18 @@ package com.datadog.gradle.config import org.gradle.api.Project import org.gradle.api.tasks.testing.Test -@Suppress("UnstableApiUsage") fun Project.junitConfig() { - tasks.withType(Test::class.java) { + tasks.withType(Test::class.java).configureEach { + jvmArgs( + "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED" + ) useJUnitPlatform { includeEngines("spek", "junit-jupiter", "junit-vintage") } reports { - junitXml.isEnabled = true - html.isEnabled = true + junitXml.required.set(true) + html.required.set(true) } } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/JacocoConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/JacocoConfig.kt deleted file mode 100644 index 5120b08552..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/JacocoConfig.kt +++ /dev/null @@ -1,85 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.config - -import com.datadog.gradle.Dependencies -import java.math.BigDecimal -import org.gradle.api.Project -import org.gradle.testing.jacoco.plugins.JacocoPluginExtension -import org.gradle.testing.jacoco.tasks.JacocoCoverageVerification -import org.gradle.testing.jacoco.tasks.JacocoReport - -fun Project.jacocoConfig() { - - val jacocoTestDebugUnitTestReport = tasks.create("jacocoTestDebugUnitTestReport", JacocoReport::class.java) - jacocoTestDebugUnitTestReport.reports { - csv.isEnabled = false - xml.isEnabled = true - html.isEnabled = true - html.destination = file("${buildDir.path}/reports/jacoco/jacocoTestDebugUnitTestReport/html") - } - - val jacocoTestReleaseUnitTestReport = tasks.create("jacocoTestReleaseUnitTestReport", JacocoReport::class.java) - jacocoTestReleaseUnitTestReport.reports { - csv.isEnabled = false - xml.isEnabled = true - html.isEnabled = true - html.destination = file("${buildDir.path}/reports/jacoco/jacocoTestReleaseUnitTestReport/html") - } - - val jacocoTestCoverageVerification = - tasks.create("jacocoTestCoverageVerification", JacocoCoverageVerification::class.java) - jacocoTestCoverageVerification.violationRules { - rule { - limit { - minimum = BigDecimal(0.85) - } - } - } - - listOf( - jacocoTestDebugUnitTestReport, - jacocoTestReleaseUnitTestReport, - jacocoTestCoverageVerification - ).forEach { task -> - val excludeFilters = arrayOf( - "**/R.class", - "**/R$*.class", - "**/BuildConfig.*", - "**/Manifest*.*", - "**/*Test*.*", - "android/**/*.*", - "**/data/models/*" - ) - - val debugTree = fileTree("${buildDir.path}/intermediates/classes/debug").apply { - exclude(*excludeFilters) - } - val kotlinDebugTree = fileTree("${buildDir.path}/tmp/kotlin-classes/debug").apply { - exclude(*excludeFilters) - } - val mainSrc = "${project.projectDir}/src/main/kotlin" - - task.classDirectories.setFrom(files(debugTree, kotlinDebugTree)) - task.executionData.setFrom(files("${buildDir.path}/jacoco/testDebugUnitTest.exec")) - task.sourceDirectories.setFrom(files(mainSrc)) - } - jacocoTestDebugUnitTestReport.dependsOn("testDebugUnitTest") - jacocoTestReleaseUnitTestReport.dependsOn("testReleaseUnitTest") - jacocoTestCoverageVerification.dependsOn(jacocoTestDebugUnitTestReport) - - extensionConfig { - toolVersion = Dependencies.Versions.Jacoco - reportsDir = file("$buildDir/jacoco") // Jacoco's output root. - } - - tasks.named("check") { - dependsOn(jacocoTestDebugUnitTestReport) - dependsOn(jacocoTestReleaseUnitTestReport) - dependsOn(jacocoTestDebugUnitTestReport) - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/JavadocConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/JavadocConfig.kt index 088178553a..8a07329a99 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/JavadocConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/JavadocConfig.kt @@ -6,27 +6,17 @@ package com.datadog.gradle.config -import com.android.build.gradle.internal.tasks.factory.dependsOn import org.gradle.api.Project -import org.gradle.jvm.tasks.Jar -import org.jetbrains.dokka.gradle.DokkaTask +import org.gradle.kotlin.dsl.configure +import org.jetbrains.dokka.gradle.DokkaExtension +import java.nio.file.Paths fun Project.javadocConfig() { - - @Suppress("UnstableApiUsage") - tasks.register("generateJavadoc", Jar::class.java) { - dependsOn("dokkaJavadoc") - archiveClassifier.convention("javadoc") - from("${buildDir.canonicalPath}/reports/javadoc") - } - - tasks.withType(DokkaTask::class.java) { - val toOutputDirectory = file("${buildDir.canonicalPath}/reports/javadoc") - outputDirectory.set(toOutputDirectory) - doFirst { - if (!toOutputDirectory.exists()) { - toOutputDirectory.mkdirs() - } + extensions.configure(DokkaExtension::class.java) { + dokkaPublications.named("javadoc") { + val toOutputDirectory = layout.buildDirectory + .dir(Paths.get("reports", "javadoc").toString()) + outputDirectory.set(toOutputDirectory) } } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/KotlinConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/KotlinConfig.kt index e0c57e10da..cd0491d440 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/KotlinConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/KotlinConfig.kt @@ -6,37 +6,22 @@ package com.datadog.gradle.config -import com.android.build.gradle.tasks.factory.AndroidUnitTest -import java.io.File -import org.gradle.api.JavaVersion import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -fun Project.kotlinConfig() { - +fun Project.kotlinConfig( + evaluateWarningsAsErrors: Boolean = true, + jvmBytecodeTarget: JvmTarget = JvmTarget.JVM_17 +) { taskConfig { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - } - - val moduleName = this@kotlinConfig.name - val javaAgentJar = File(File(rootDir, "libs"), "dd-java-agent-0.67.0.jar") - taskConfig { - if (environment["DD_INTEGRATION_JUNIT_5_ENABLED"] == "true") { - val variant = variantName.substringBeforeLast("UnitTest") - - // set the `env` tag for the test spans - environment("DD_ENV", "ci") - // add custom tags based on the module and variant (debug/release, flavors, …) - environment("DD_TAGS", "test.module:$moduleName,test.variant:$variant") - - // disable other Datadog integrations that could interact with the Java Agent - environment("DD_INTEGRATIONS_ENABLED", "false") - // disable JMX integration - environment("DD_JMX_FETCH_ENABLED", "false") - - jvmArgs("-javaagent:${javaAgentJar.absolutePath}") + compilerOptions { + jvmTarget.set(jvmBytecodeTarget) + val isCI = System.getenv("CI").toBoolean() + allWarningsAsErrors.set(evaluateWarningsAsErrors && isCI) + apiVersion.set(KotlinVersion.KOTLIN_1_8) + languageVersion.set(KotlinVersion.KOTLIN_1_8) } } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/KtLintConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/KtLintConfig.kt deleted file mode 100644 index cca6af129d..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/KtLintConfig.kt +++ /dev/null @@ -1,31 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.config - -import org.gradle.api.Project -import org.jlleitschuh.gradle.ktlint.KtlintExtension - -fun Project.ktLintConfig() { - - extensionConfig { - debug.set(false) - android.set(true) - outputToConsole.set(true) - ignoreFailures.set(false) - enableExperimentalRules.set(false) - additionalEditorconfigFile.set(file("${project.rootDir}/script/config/.editorconfig")) - filter { - exclude("**/generated/**") - exclude("**/com/datadog/android/rum/internal/domain/model/**") - include("**/kotlin/**") - } - } - - tasks.named("check") { - dependsOn("ktlintCheck") - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/MavenConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/MavenConfig.kt index 9734f4eaeb..fb7f0222bf 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/MavenConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/MavenConfig.kt @@ -6,6 +6,95 @@ package com.datadog.gradle.config +import com.android.build.gradle.LibraryExtension +import org.gradle.api.Project +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.kotlin.dsl.findByType +import org.gradle.plugins.signing.SigningExtension + object MavenConfig { const val GROUP_ID = "com.datadoghq" + const val PUBLICATION = "release" +} + +fun Project.publishingConfig( + projectDescription: String, + customArtifactId: String = name +) { + val projectName = name + + val androidExtension = + extensions.findByType(LibraryExtension::class.java) + if (androidExtension == null) { + logger.error("Missing android library extension for $projectName") + return + } + + androidExtension.publishing { + singleVariant(MavenConfig.PUBLICATION) { + withSourcesJar() + withJavadocJar() + } + } + + afterEvaluate { + val publishingExtension = extensions.findByType(PublishingExtension::class) + val signingExtension = extensions.findByType(SigningExtension::class) + if (publishingExtension == null || signingExtension == null) { + System.err.println("Missing publishing or signing extension for $projectName") + return@afterEvaluate + } + + publishingExtension.apply { + publications.create(MavenConfig.PUBLICATION, MavenPublication::class.java) { + from(components.getByName("release")) + + groupId = MavenConfig.GROUP_ID + artifactId = customArtifactId + version = AndroidConfig.VERSION.name + + pom { + name.set(projectName) + description.set(projectDescription) + url.set("https://github.com/DataDog/dd-sdk-android/") + + licenses { + license { + name.set("Apache-2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0") + } + } + + organization { + name.set("Datadog") + url.set("https://www.datadoghq.com/") + } + + developers { + developer { + name.set("Datadog") + email.set("info@datadoghq.com") + organization.set("Datadog") + organizationUrl.set("https://www.datadoghq.com/") + } + } + + scm { + url.set("https://github.com/DataDog/dd-sdk-android/") + connection.set("scm:git:git@github.com:Datadog/dd-sdk-android.git") + developerConnection.set("scm:git:git@github.com:Datadog/dd-sdk-android.git") + } + } + } + } + + signingExtension.apply { + val privateKey = System.getenv("GPG_PRIVATE_KEY") + val password = System.getenv("GPG_PASSWORD") + isRequired = !hasProperty("dd-skip-signing") + useInMemoryPgpKeys(privateKey, password) + sign(publishingExtension.publications.getByName(MavenConfig.PUBLICATION)) + } + } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/PublishingConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/PublishingConfig.kt deleted file mode 100644 index 2830744b23..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/PublishingConfig.kt +++ /dev/null @@ -1,108 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.config - -import com.jfrog.bintray.gradle.BintrayExtension -import org.gradle.api.Project -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven -import org.gradle.jvm.tasks.Jar -import org.gradle.kotlin.dsl.delegateClosureOf - -const val MAVEN_PUBLICATION = "aar" -const val BINTRAY_USER = "bintrayUser" -const val BINTRAY_API_KEY = "bintrayApiKey" - -fun Project.publishingConfig(localRepo: String) { - - version = AndroidConfig.VERSION.name - group = MavenConfig.GROUP_ID - - val projectName = name - - extensionConfig { - repositories { - maven { - setUrl(localRepo) - } - } - - publications { - register(MAVEN_PUBLICATION, MavenPublication::class.java) { - groupId = MavenConfig.GROUP_ID - artifactId = projectName - version = AndroidConfig.VERSION.name - - artifact("$buildDir/outputs/aar/$projectName-release.aar") - artifact(tasks.findByName("sourcesJar")) - artifact(tasks.findByName("generateJavadoc")) - - // publishing AAR doesn't fill the pom.xml dependencies. - pom.withXml { - val dependenciesNode = asNode().appendNode("dependencies") - configurations.named("implementation").get().allDependencies.forEach { - val dependencyNode = dependenciesNode.appendNode("dependency") - dependencyNode.appendNode("groupId", it.group) - dependencyNode.appendNode("artifactId", it.name) - dependencyNode.appendNode("version", it.version) - } - } - } - } - } - - @Suppress("UnstableApiUsage") - tasks.register("sourcesJar", Jar::class.java) { - archiveClassifier.convention("sources") - from("${projectDir.canonicalPath}/src/main") - } - - tasks.withType(AbstractPublishToMaven::class.java) { - this.dependsOn("bundleReleaseAar") - this.dependsOn("sourcesJar") - this.dependsOn("generateJavadoc") - } - - task("publishLocalAndRemote").apply { - this.group = "publishing" - this.dependsOn("publish") - this.dependsOn("publishToMavenLocal") - } -} - -fun Project.bintrayConfig() { - val projectName = name - - extensionConfig { - - user = this@bintrayConfig.findProperty(BINTRAY_USER)?.toString() - key = this@bintrayConfig.findProperty(BINTRAY_API_KEY)?.toString() - - setPublications(MAVEN_PUBLICATION) - - // dryRun = true - override = true - publish = true - - pkg(delegateClosureOf { - repo = "datadog-maven" - name = projectName - userOrg = "datadog" - desc = "Datadog SDK fot Android" - websiteUrl = "https://www.datadoghq.com/" - setLicenses("Apache-2.0") - githubRepo = "DataDog/dd-sdk-android" - githubReleaseNotesFile = "README.md" - vcsUrl = "https://github.com/DataDog/dd-sdk-android.git" - - version(delegateClosureOf { - name = AndroidConfig.VERSION.name - }) - }) - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/SampleAppConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/SampleAppConfig.kt index 0dc34fd696..bdad42212e 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/SampleAppConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/SampleAppConfig.kt @@ -10,6 +10,7 @@ data class SampleAppConfig( val logsEndpoint: String = "", val tracesEndpoint: String = "", val rumEndpoint: String = "", + val sessionReplayEndpoint: String = "", val token: String = "", val rumApplicationId: String = "", val apiKey: String = "", diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/TestPyramidConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/TestPyramidConfig.kt new file mode 100644 index 0000000000..247c537863 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/TestPyramidConfig.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.config + +import org.gradle.api.Project +import org.gradle.api.Task + +fun Project.registerSubModuleAggregationTask( + taskName: String, + subModuleTaskName: String, + subModulePathPrefix: String = ":", + subModuleNamePrefix: String = "dd-sdk-android-", + exceptions: Set = emptySet(), + additionalConfiguration: Task.() -> Unit = {} +) { + tasks.register(taskName) { + project.subprojects.forEach { subProject -> + if (!exceptions.contains(subProject.name) && + subProject.name.startsWith(subModuleNamePrefix) && + subProject.path.startsWith(subModulePathPrefix) + ) { + dependsOn("${subProject.path}:$subModuleTaskName") + } + } + additionalConfiguration() + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/CheckGeneratedFileTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/CheckGeneratedFileTask.kt new file mode 100644 index 0000000000..51f8abf5c3 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/CheckGeneratedFileTask.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin + +import com.datadog.gradle.utils.execShell +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Internal +import org.gradle.process.ExecOperations +import java.io.File + +abstract class CheckGeneratedFileTask( + @Internal val genTaskName: String, + private val execOperations: ExecOperations +) : DefaultTask() { + + // region Task + + fun verifyGeneratedFileExists(targetFile: File) { + val lines = execOperations.execShell( + "git", + "diff", + "--color=never", + "HEAD", + "--", + targetFile.absolutePath + ) + + val additions = lines.filter { it.matches(Regex("^\\+[^+].*$")) } + val removals = lines.filter { it.matches(Regex("^-[^-].*$")) } + + if (additions.isNotEmpty() || removals.isNotEmpty()) { + error( + "Make sure you run the $genTaskName task before you push your PR.\n" + + additions.joinToString("\n") + + "\n" + + removals.joinToString("\n") + ) + } + } + + // endregion +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/ApiSurfacePlugin.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/ApiSurfacePlugin.kt index 2580e05f21..795f063962 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/ApiSurfacePlugin.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/ApiSurfacePlugin.kt @@ -7,35 +7,55 @@ package com.datadog.gradle.plugin.apisurface import com.datadog.gradle.config.taskConfig -import java.io.File import org.gradle.api.Plugin import org.gradle.api.Project import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.File +import java.nio.file.Paths class ApiSurfacePlugin : Plugin { override fun apply(target: Project) { - val srcDir = File(target.projectDir, "src") - val surfaceFile = File(target.projectDir, FILE_NAME) - - val generateTask = target.tasks - .create(TASK_GEN_API_SURFACE, GenerateApiSurfaceTask::class.java) - generateTask.srcDir = File(srcDir, "main") - generateTask.surfaceFile = surfaceFile - - val checkTask = target.tasks - .create(TASK_CHECK_API_SURFACE, CheckApiSurfaceTask::class.java) - checkTask.surfaceFile = surfaceFile - checkTask.dependsOn(TASK_GEN_API_SURFACE) + val srcDir = File(File(target.projectDir, "src"), "main") + val genDir = target.layout.buildDirectory + .dir(Paths.get("generated", "json2kotlin").toString()) + .get() + .asFile + val apiDir = File(target.projectDir, "api") + val kotlinSurfaceFile = File(apiDir, FILE_NAME) + val javaSurfaceFile = File(apiDir, "${target.name}.api") + + target.tasks + .register(TASK_GEN_KOTLIN_API_SURFACE, GenerateApiSurfaceTask::class.java) { + this.srcDirPath = srcDir.absolutePath + this.genDirPath = genDir.absolutePath + this.surfaceFile = kotlinSurfaceFile + } + target.tasks + .register(TASK_CHECK_API_SURFACE, CheckApiSurfaceTask::class.java) { + this.kotlinSurfaceFile = kotlinSurfaceFile + this.javaSurfaceFile = javaSurfaceFile + dependsOn(TASK_GEN_KOTLIN_API_SURFACE) + if (target.plugins.hasPlugin(GEN_JAVA_API_LAYOUT_PLUGIN)) { + dependsOn(TASK_GEN_JAVA_API_SURFACE) + } + } target.taskConfig { - finalizedBy(TASK_GEN_API_SURFACE) + // Java API generation task does a clean-up of all files in the output + // folder, so let it run first + if (target.plugins.hasPlugin(GEN_JAVA_API_LAYOUT_PLUGIN)) { + finalizedBy(TASK_GEN_JAVA_API_SURFACE) + } + finalizedBy(TASK_GEN_KOTLIN_API_SURFACE) } } companion object { - const val TASK_GEN_API_SURFACE = "generateApiSurface" + const val TASK_GEN_KOTLIN_API_SURFACE = "generateApiSurface" + const val TASK_GEN_JAVA_API_SURFACE = "apiDump" const val TASK_CHECK_API_SURFACE = "checkApiSurfaceChanges" const val FILE_NAME = "apiSurface" + const val GEN_JAVA_API_LAYOUT_PLUGIN = "binary-compatibility-validator" } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/CheckApiSurfaceTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/CheckApiSurfaceTask.kt index 77f0d352d5..be85ed50b2 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/CheckApiSurfaceTask.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/CheckApiSurfaceTask.kt @@ -6,16 +6,26 @@ package com.datadog.gradle.plugin.apisurface -import com.datadog.gradle.utils.execShell -import java.io.File -import org.gradle.api.DefaultTask +import com.datadog.gradle.plugin.CheckGeneratedFileTask import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import java.io.File +import javax.inject.Inject -open class CheckApiSurfaceTask : DefaultTask() { +open class CheckApiSurfaceTask @Inject constructor( + execOperations: ExecOperations +) : CheckGeneratedFileTask( + genTaskName = ApiSurfacePlugin.TASK_GEN_KOTLIN_API_SURFACE, + execOperations +) { @InputFile - lateinit var surfaceFile: File + lateinit var kotlinSurfaceFile: File + + @InputFiles + lateinit var javaSurfaceFile: File init { group = "datadog" @@ -26,23 +36,14 @@ open class CheckApiSurfaceTask : DefaultTask() { @TaskAction fun applyTask() { - val lines = project.execShell( - "git", "diff", "--color=never", "HEAD", "--", surfaceFile.absolutePath - ) - - val additions = lines.filter { it.matches(Regex("^\\+[^+].*$")) } - val removals = lines.filter { it.matches(Regex("^-[^-].*$")) } - - if (additions.isNotEmpty() || removals.isNotEmpty()) { - throw IllegalStateException( - "Make sure you run the ${ApiSurfacePlugin.TASK_GEN_API_SURFACE} task before you push your PR.\n" + - additions.joinToString("\n") + removals.joinToString("\n") - ) + verifyGeneratedFileExists(kotlinSurfaceFile) + if (javaSurfaceFile.exists()) { + verifyGeneratedFileExists(javaSurfaceFile) } } @InputFile - fun getInputFile() = surfaceFile + fun getInputFile() = kotlinSurfaceFile // endregion } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/GenerateApiSurfaceTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/GenerateApiSurfaceTask.kt index 4f5009e23d..5362a93510 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/GenerateApiSurfaceTask.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/GenerateApiSurfaceTask.kt @@ -6,18 +6,22 @@ package com.datadog.gradle.plugin.apisurface -import java.io.File import org.gradle.api.DefaultTask -import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction +import java.io.File open class GenerateApiSurfaceTask : DefaultTask() { - @get:InputDirectory - lateinit var srcDir: File + @get:Input + lateinit var srcDirPath: String + + @get:Input + lateinit var genDirPath: String @get: OutputFile lateinit var surfaceFile: File + private lateinit var visitor: KotlinFileVisitor init { @@ -30,7 +34,8 @@ open class GenerateApiSurfaceTask : DefaultTask() { @TaskAction fun applyTask() { visitor = KotlinFileVisitor() - visitDirectoryRecursively(srcDir) + visitDirectoryRecursively(File(srcDirPath)) + visitDirectoryRecursively(File(genDirPath)) surfaceFile.printWriter().use { it.print(visitor.description.toString()) @@ -41,11 +46,13 @@ open class GenerateApiSurfaceTask : DefaultTask() { private fun visitDirectoryRecursively(file: File) { when { - file.isDirectory -> file.listFiles().orEmpty() - .sortedBy { it.absolutePath } - .forEach { visitDirectoryRecursively(it) } + !file.exists() -> logger.info("File $file doesn't exist, ignoring") + file.isDirectory -> + file.listFiles().orEmpty() + .sortedBy { it.absolutePath } + .forEach { visitDirectoryRecursively(it) } file.isFile -> visitFile(file) - else -> System.err.println("${file.path} is neither file nor directory") + else -> logger.error("${file.path} is neither file nor directory") } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitor.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitor.kt index 8e4ae7ca57..ce4976dad0 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitor.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitor.kt @@ -6,13 +6,13 @@ package com.datadog.gradle.plugin.apisurface -import java.io.File import kotlinx.ast.common.AstSource import kotlinx.ast.common.ast.Ast import kotlinx.ast.common.ast.AstNode import kotlinx.ast.common.ast.AstTerminal import kotlinx.ast.common.print import kotlinx.ast.grammar.kotlin.target.antlr.kotlin.KotlinGrammarAntlrKotlinParser +import java.io.File class KotlinFileVisitor { @@ -22,9 +22,10 @@ class KotlinFileVisitor { // region KotlinFileVisitor + @Suppress("TooGenericExceptionCaught") fun visitFile(file: File, printAst: Boolean = false) { val code = file.readText() - val source = AstSource.String(code) + val source = AstSource.String(description = "Content of file ${file.path}", content = code) val ast = KotlinGrammarAntlrKotlinParser.parseKotlinFile(source) if (printAst) ast.print() @@ -44,7 +45,7 @@ class KotlinFileVisitor { when (ast) { is AstNode -> visitAstNode(ast, level) is AstTerminal -> ignoreNode() - else -> throw IllegalStateException("Unable to handle $ast") + else -> error("Unable to handle $ast") } } @@ -82,6 +83,7 @@ class KotlinFileVisitor { // Modifiers if (node.isDeprecated()) description.append("DEPRECATED ") + if (node.isDataClass()) description.append("data ") if (node.isSealed()) description.append("sealed ") if (node.isProtected()) description.append("protected ") if (node.isOpen()) description.append("open ") @@ -90,6 +92,7 @@ class KotlinFileVisitor { when { node.hasChildTerminal("INTERFACE") -> description.append("interface ") node.isEnum() -> description.append("enum ") + node.isAnnotation() -> description.append("annotation ") else -> description.append("$type ") } @@ -173,6 +176,11 @@ class KotlinFileVisitor { description.append(" ") } + visitReceiver(node) + if (node.hasChildNode("receiverType")) { + description.append(".") + } + // Name description.append(node.identifierName()) @@ -262,6 +270,11 @@ class KotlinFileVisitor { ) } + private fun visitReceiver(node: AstNode) { + val receiverType = node.firstChildNodeOrNull("receiverType") ?: return + description.append(receiverType.typeName()) + } + private fun visitFunctionParameters(node: AstNode) { description.append("(") description.append( @@ -307,7 +320,12 @@ class KotlinFileVisitor { val type = node.firstChildNode("type") description.append(INDENT.repeat(level)) - description.append("typealias $name = ${type.lambdaName()}\n") + val rootTypeName = if (type.hasChildNode("functionType")) { + type.lambdaName() + } else { + type.typeName() + } + description.append("typealias $name = $rootTypeName\n") } private fun ignoreNode() { @@ -322,10 +340,18 @@ class KotlinFileVisitor { return hasModifier("classModifier", "SEALED") } + private fun AstNode.isDataClass(): Boolean { + return hasModifier("classModifier", "DATA") + } + private fun AstNode.isEnum(): Boolean { return hasModifier("classModifier", "ENUM") } + private fun AstNode.isAnnotation(): Boolean { + return hasModifier("classModifier", "ANNOTATION") + } + private fun AstNode.isProtected(): Boolean { return hasModifier("visibilityModifier", "PROTECTED") } @@ -406,12 +432,6 @@ class KotlinFileVisitor { return this is AstTerminal && this.description == description } - private fun AstNode.firstChildTerminal(description: String): AstTerminal { - val first = firstChildTerminalOrNull(description) - checkNotNull(first) { "Unable to find a child with description $description in \n$this" } - return first - } - private fun AstNode.firstChildTerminalOrNull(description: String): AstTerminal? { return children.firstOrNull { it.isTerminal(description) } as? AstTerminal } @@ -450,7 +470,8 @@ class KotlinFileVisitor { val generics = userType.firstChildNodeOrNull("typeArguments") ?.childrenNodes("typeProjection") ?.joinToString(", ", prefix = "<", postfix = ">") { - it.firstChildNode("type").typeName() + it.firstChildNodeOrNull("type")?.typeName() + ?: it.firstChildTerminalOrNull("MULT")?.text.toString() } ?: "" if (aggr.isEmpty()) { (imports[typeName] ?: typeName) + generics @@ -464,9 +485,11 @@ class KotlinFileVisitor { val functionType = firstChildNode("functionType") val receiver = functionType.firstChildNodeOrNull("receiverType")?.typeName() - val params = functionType.firstChildNode("functionTypeParameters") - .childrenNodes("type") - .joinToString(", ") { it.typeName() } + val functionTypeParamsNode = functionType.firstChildNode("functionTypeParameters") + val params = + (functionTypeParamsNode.firstChildNodeOrNull("parameter") ?: functionTypeParamsNode) + .childrenNodes("type") + .joinToString(", ") { it.typeName() } val returns = functionType.firstChildNode("type").typeName() return if (receiver == null) { diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/BenchmarkJsonFileVisitor.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/BenchmarkJsonFileVisitor.kt deleted file mode 100644 index 7f43815eff..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/BenchmarkJsonFileVisitor.kt +++ /dev/null @@ -1,49 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark - -import com.datadog.gradle.plugin.benchmark.model.Benchmark -import com.datadog.gradle.plugin.benchmark.model.BenchmarkResult -import com.google.gson.Gson -import java.io.File - -class BenchmarkJsonFileVisitor { - - // region ReviewJsonFileVisitor - - fun visitBenchmarkJsonFile( - jsonFile: File, - benchmarksAccumulator: MutableMap - ) { - val gson = Gson() - val benchmarkResult = gson.fromJson(jsonFile.readText(), BenchmarkResult::class.java) - - println(benchmarkResult.context.targetBuild) - - benchmarkResult.benchmarks.forEach { - benchmarksAccumulator[it.nameWithoutPrefixes()] = it.metrics.timeNs.median - } - } - - // endregion - - // region Internal - - private fun Benchmark.nameWithoutPrefixes(): String { - return devicePrefixes.fold(name) { acc, prefix -> - acc.replace("${prefix}_", "") - } - } - - // endregion - - companion object { - private val devicePrefixes = listOf( - "EMULATOR", "UNLOCKED", "DEBUGGABLE", "ENG-BUILD" - ) - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/BenchmarkStrategy.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/BenchmarkStrategy.kt deleted file mode 100644 index b1c8a8f05d..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/BenchmarkStrategy.kt +++ /dev/null @@ -1,75 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark - -import java.util.concurrent.TimeUnit - -sealed class BenchmarkStrategy { - - abstract fun verify(benchmarkResults: Map): Boolean - - class AbsoluteThreshold(val compare: String, val threshold: Long) : BenchmarkStrategy() { - override fun verify(benchmarkResults: Map): Boolean { - val toMeasure = getOrThrow(benchmarkResults, compare) - return when { - toMeasure <= threshold -> true - else -> { - System.err.println( - "Benchmark test \"$$compare\" reported a median time of " + - "$toMeasure milliseconds, but threshold is set to " + - "$threshold milliseconds" - ) - false - } - } - } - } - - class RelativeThreshold( - val compare: String, - val compareTo: String, - val threshold: Long - ) : BenchmarkStrategy() { - override fun verify(benchmarkResults: Map): Boolean { - - val medianA = getOrThrow(benchmarkResults, compare) - val medianB = getOrThrow(benchmarkResults, compareTo) - val toMeasure = Math.abs(medianA - medianB) - return when { - toMeasure <= threshold -> true - else -> { - System.err.println( - "We were expecting a relativeThreshold smaller or equal with $threshold " + - "milliseconds between the benchmark : \"$compare\" and : " + - "\"$compareTo\" instead it was of $toMeasure milliseconds" - ) - false - } - } - } - } - - internal fun getOrThrow(benchmarkResults: Map, key: String): Long { - val l = benchmarkResults[key] - checkNotNull(l) { - System.err.println( - "There was no benchmark for test \"$key\"" - ) - } - return l.toMillis() - } - - object Ignore : BenchmarkStrategy() { - override fun verify(benchmarkResults: Map): Boolean { - return true - } - } - - private fun Long.toMillis(): Long { - return TimeUnit.NANOSECONDS.toMillis(this) - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkExtension.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkExtension.kt deleted file mode 100644 index a6ce1ce806..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkExtension.kt +++ /dev/null @@ -1,27 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark - -open class ReviewBenchmarkExtension { - - internal val benchmarkStrategies = mutableMapOf() - fun addThreshold(name: String, threshold: Long) { - benchmarkStrategies[name] = BenchmarkStrategy.AbsoluteThreshold(name, threshold) - } - - fun ignoreTest(name: String) { - benchmarkStrategies[name] = BenchmarkStrategy.Ignore - } - - fun relativeThreshold(betweenTest: String, andTest: String, shouldNotExceed: Long) { - benchmarkStrategies[betweenTest] = - BenchmarkStrategy.RelativeThreshold(betweenTest, andTest, shouldNotExceed) - // add an ignore strategy for the second in order to not be matched - // as a no - strategy benchmark - benchmarkStrategies[andTest] = BenchmarkStrategy.Ignore - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkPlugin.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkPlugin.kt deleted file mode 100644 index 1dd4560c26..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkPlugin.kt +++ /dev/null @@ -1,34 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark - -import org.gradle.api.Plugin -import org.gradle.api.Project - -class ReviewBenchmarkPlugin : Plugin { - - // region Plugin - - override fun apply(target: Project) { - val reviewExtension = target.extensions - .create(EXTENSION_NAME, ReviewBenchmarkExtension::class.java) - - val reviewTask = target.tasks - .create(TASK_REVIEW_NAME, ReviewBenchmarkResultsTask::class.java) - reviewTask.buildDir = target.buildDir - reviewTask.extension = reviewExtension - - reviewTask.dependsOn("connectedCheck") - } - - // endregion - - companion object { - const val EXTENSION_NAME = "reviewBenchmark" - const val TASK_REVIEW_NAME = "reviewBenchmarkResults" - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkResultsTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkResultsTask.kt deleted file mode 100644 index 1dcbd97ddb..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/ReviewBenchmarkResultsTask.kt +++ /dev/null @@ -1,87 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark - -import java.io.File -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.TaskAction - -open class ReviewBenchmarkResultsTask : DefaultTask() { - - @get: Input - internal lateinit var extension: ReviewBenchmarkExtension - @get: InputDirectory - internal lateinit var buildDir: File - @get: Input - val visitor = BenchmarkJsonFileVisitor() - - init { - group = "datadog" - description = - "Review the benchmark results and ensure they are below the specified threshold" - } - - // region Task - - @TaskAction - fun applyTask() { - val targetDir = getInputDir() - if (targetDir.exists() && targetDir.canRead()) { - targetDir.listFiles()?.forEach { - processBenchmarksPerDevice(it) - } - } - } - - @InputDirectory - fun getInputDir(): File { - val outputsDir = File(buildDir, "outputs") - return File( - outputsDir, - "connected_android_test_additional_output/debugAndroidTest/connected" - ) - } - - // endregion - - // region Internal - - private fun processBenchmarksPerDevice( - deviceDir: File - ) { - println("processing benchmark results for device ${deviceDir.name}") - if (deviceDir.exists() && deviceDir.isDirectory && deviceDir.canRead()) { - val benchmarksResults = mutableMapOf() - deviceDir.listFiles()?.forEach { - if (it.isFile && it.canRead() && it.extension == "json") { - visitor.visitBenchmarkJsonFile(it, benchmarksResults) - } - } - - val strategies = extension.benchmarkStrategies - - // verify that all the benchmarks have at least one strategy and - // then verify the strategy - benchmarksResults.forEach { - val strategy = strategies[it.key] - println( - "Reviewing benchmark result: ${it.key} with" + - " strategy: ${strategy?.javaClass?.simpleName}" - ) - checkNotNull(strategy) { - System.err.println("No benchmarking strategy added for test \"${it.key}\"") - } - - check(strategy.verify(benchmarksResults)) - } - } - } - - // endregion -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/Benchmark.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/Benchmark.kt deleted file mode 100644 index 818c35b436..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/Benchmark.kt +++ /dev/null @@ -1,19 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark.model - -import com.google.gson.annotations.SerializedName - -data class Benchmark( - @SerializedName("name") var name: String, - @SerializedName("className") var className: String, - @SerializedName("totalRunTimeNs") var totalRunTimeNs: Long, - @SerializedName("warmupIterations") var warmupIterations: Int, - @SerializedName("repeatIterations") var repeatIterations: Int, - @SerializedName("thermalThrottleSleepSeconds") var thermalThrottleSleepSeconds: Int, - @SerializedName("metrics") var metrics: BenchmarkMetrics -) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkContext.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkContext.kt deleted file mode 100644 index c6ac494376..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkContext.kt +++ /dev/null @@ -1,18 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark.model - -import com.google.gson.annotations.SerializedName - -data class BenchmarkContext( - @SerializedName("build") var targetBuild: BenchmarkTargetBuild, - @SerializedName("cpuCoreCount") var cpuCoreCount: Int, - @SerializedName("cpuLocked") var cpuLocked: Boolean, - @SerializedName("cpuMaxFreqHz") var cpuMaxFreqHz: Int, - @SerializedName("memTotalBytes") var memTotalBytes: Long, - @SerializedName("sustainedPerformanceModeEnabled") var sustainedPerformanceModeEnabled: Boolean -) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkMetrics.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkMetrics.kt deleted file mode 100644 index 8a746ec756..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkMetrics.kt +++ /dev/null @@ -1,13 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark.model - -import com.google.gson.annotations.SerializedName - -data class BenchmarkMetrics( - @SerializedName("timeNs") var timeNs: BenchmarkTimeMetrics -) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkResult.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkResult.kt deleted file mode 100644 index 3e874c74cb..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkResult.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark.model - -import com.google.gson.annotations.SerializedName - -data class BenchmarkResult( - @SerializedName("context") var context: BenchmarkContext, - @SerializedName("benchmarks") var benchmarks: List -) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkTargetBuild.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkTargetBuild.kt deleted file mode 100644 index 3954450d46..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkTargetBuild.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark.model - -import com.google.gson.annotations.SerializedName - -data class BenchmarkTargetBuild( - @SerializedName("device") var device: String, - @SerializedName("fingerprint") var fingerprint: String, - @SerializedName("model") var model: String, - @SerializedName("version") var version: Version -) { - data class Version( - @SerializedName("version") var sdk: Int - ) -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkTimeMetrics.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkTimeMetrics.kt deleted file mode 100644 index 51ac6d69c9..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/benchmark/model/BenchmarkTimeMetrics.kt +++ /dev/null @@ -1,16 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.benchmark.model - -import com.google.gson.annotations.SerializedName - -data class BenchmarkTimeMetrics( - @SerializedName("minimum") var minimum: Long, - @SerializedName("maximum") var maximum: Long, - @SerializedName("median") var median: Long, - @SerializedName("runs") var runs: List -) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/CheckThirdPartyLicensesTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/CheckThirdPartyLicensesTask.kt deleted file mode 100644 index 501daab328..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/CheckThirdPartyLicensesTask.kt +++ /dev/null @@ -1,129 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -import java.io.File -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.TaskAction - -open class CheckThirdPartyLicensesTask : DefaultTask() { - - @get:Input - internal var extension: ThirdPartyLicensesExtension = - ThirdPartyLicensesExtension() - private val provider: DependenciesLicenseProvider = - DependenciesLicenseProvider() - - init { - group = "datadog" - description = "Check all Third Party Licences appear in the csv file" - } - - // region Task - - @TaskAction - fun applyTask() { - - val projectDependencies = provider.getThirdPartyDependencies( - project, - extension.transitiveDependencies, - extension.listDependencyOnce - ) - val listedDependencies = parseCsvFile() - - checkMatchingDependencies(projectDependencies, listedDependencies, "missing") - - if (extension.checkObsoleteDependencies) { - checkMatchingDependencies(listedDependencies, projectDependencies, "obsolete") - } - - listedDependencies.filter { it.license is License.Empty } - .forEach { - System.err.println("License for ${it.origin} is empty") - } - - listedDependencies.filter { it.license is License.Raw } - .forEach { - System.err.println("License for ${it.origin} is not valid : ${it.license}") - } - - listedDependencies.filter { it.copyright == "__" } - .forEach { - System.err.println("Copyright for ${it.origin} is missing") - } - } - - private fun checkMatchingDependencies( - trueDependencies: List, - testedDependencies: List, - check: String - ) { - var error = false - - trueDependencies.forEach { dep -> - val known = testedDependencies.firstOrNull { - it.component == dep.component && it.origin == dep.origin - } - val knownInOtherComponent = testedDependencies.firstOrNull { - it.component != dep.component && it.origin == dep.origin - } - - if (known == null && knownInOtherComponent == null) { - error = true - System.err.println("✗ $check dependency in ${extension.csvFile.name} : $dep") - } else if (knownInOtherComponent != null) { - System.err.println("✗ $dep $check but exist in component ${knownInOtherComponent.component}") - } - } - - check(!error) { "Some dependencies are missing in ${extension.csvFile.name}" } - } - - @InputFile - fun getCsvInputFile(): File { - return extension.csvFile - } - - // endregion - - // region Internal - - private fun parseCsvFile(): List { - val result = mutableListOf() - var firstLineRead = false - extension.csvFile.forEachLine { - if (firstLineRead) { - val (component, origin, license, copyright) = it.split(",") - result.add( - ThirdPartyDependency( - component = componentMap[component] - ?: ThirdPartyDependency.Component.UNKNOWN, - origin = origin, - license = License.from( - license - ), - copyright = copyright - ) - ) - } else { - firstLineRead = true - } - } - - return result - } - - // endregion - - companion object { - private val componentMap = ThirdPartyDependency.Component.values() - .map { it.csvName to it } - .toMap() - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/DependenciesLicenseProvider.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/DependenciesLicenseProvider.kt deleted file mode 100644 index 8cf263b4fb..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/DependenciesLicenseProvider.kt +++ /dev/null @@ -1,227 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -import com.datadog.gradle.utils.asSequence -import javax.xml.parsers.DocumentBuilderFactory -import org.gradle.api.Project -import org.gradle.api.artifacts.component.ComponentIdentifier -import org.gradle.api.artifacts.result.ComponentSelectionCause -import org.gradle.api.artifacts.result.ResolvedArtifactResult -import org.gradle.api.artifacts.result.ResolvedDependencyResult -import org.gradle.maven.MavenModule -import org.gradle.maven.MavenPomArtifact -import org.w3c.dom.Document - -class DependenciesLicenseProvider { - - fun getThirdPartyDependencies( - project: Project, - transitive: Boolean, - listDependencyOnce: Boolean - ): List { - val dependencies = getConfigurationDependenciesMap(project, transitive) - - val dependencyIds = dependencies.values.flatten() - val pomFilesList = resolvePomFiles(project, dependencyIds) - - return listThirdPartyLicenses(dependencies, pomFilesList, listDependencyOnce) - } - - // region Internal - - private fun getConfigurationDependenciesMap( - project: Project, - transitive: Boolean - ): Map> { - return project.configurations.filter { it.isCanBeResolved } - .map { configuration -> - configuration.name to configuration.incoming.resolutionResult.allDependencies - .filterIsInstance() - .filter { transitive || it.isRoot() } - .map { it.selected.id } - } - .filter { it.second.isNotEmpty() } - .toMap() - } - - private fun resolvePomFiles( - project: Project, - dependencyIds: List - ): Map { - return project.dependencies - .createArtifactResolutionQuery() - .withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java) - .forComponents(dependencyIds) - .execute() - .resolvedComponents - .flatMap { result -> - result.getArtifacts(MavenPomArtifact::class.java) - .filterIsInstance() - .map { result.id to it.file.absolutePath } - }.toMap() - } - - private fun listThirdPartyLicenses( - dependencies: Map>, - pomFilesList: Map, - listDependencyOnce: Boolean - ): List { - val sorted = dependencies.map { - listThridPartyLicensesInConfiguration( - it.key, - it.value, - pomFilesList - ) - }.flatten() - .toSet() - .sortedBy { it.origin } - .sortedBy { it.component.ordinal } - - return if (listDependencyOnce) { - val knownOrigins = mutableSetOf() - val result = mutableListOf() - sorted.forEach { - if (it.origin !in knownOrigins) { - result.add(it) - knownOrigins.add(it.origin) - } else { - println("Ignoring ${it.component.csvName}/${it.origin}, already added.") - } - } - result - } else { - sorted - } - } - - private fun listThridPartyLicensesInConfiguration( - configuration: String, - dependencies: List, - pomFilesList: Map - ): List { - return dependencies.mapNotNull { - val pomFilePath = pomFilesList[it] - if (pomFilePath.isNullOrBlank()) { - System.err.println("Missing pom.xml file for dependency $it") - null - } else { - readLicenseFromPomFile(configuration, pomFilePath) - } - } - } - - private fun readLicenseFromPomFile( - configuration: String, - path: String - ): ThirdPartyDependency? { - val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(path) - val groupId = readGroupIdFromPomDocument(document) - val licenseString = readLicenseFromPomDocument(document) - - return if (groupId != null) { - return ThirdPartyDependency( - component = configurationToComponent(configuration), - origin = groupId, - license = License.from( - licenseString - ), - copyright = "__" - ) - } else { - System.err.println("Missing groupId in $path") - null - } - } - - private fun configurationToComponent(configuration: String): ThirdPartyDependency.Component { - if (configuration in knownImportConfiguration) { - return ThirdPartyDependency.Component.IMPORT - } else if (configuration in knownImportTestConfiguration) { - return ThirdPartyDependency.Component.IMPORT_TEST - } else if (configuration in knownBuildConfiguration) { - return ThirdPartyDependency.Component.BUILD - } else { - System.err.println("Unknown configuration $configuration") - return ThirdPartyDependency.Component.UNKNOWN - } - } - - private fun readGroupIdFromPomDocument(document: Document): String? { - val groupIdNode = document.getElementsByTagName(TAG_GROUP_ID) - .asSequence().firstOrNull() - val groupId = groupIdNode?.textContent - return groupId - } - - private fun readLicenseFromPomDocument(document: Document): String? { - val licencesNode = document.getElementsByTagName(TAG_LICENSES) - .asSequence() - .firstOrNull() - val licenceNodes = licencesNode?.childNodes - ?.asSequence() - ?.filter { it.nodeName == TAG_LICENSE } - val licenses = licenceNodes?.asSequence() - ?.mapNotNull { - it.childNodes - .asSequence() - .firstOrNull { child -> child.nodeName == TAG_NAME } - } - ?.joinToString("/") { it.textContent } - return licenses - } - - @Suppress("UnstableApiUsage") - private fun ResolvedDependencyResult.isRoot(): Boolean { - return from.selectionReason.descriptions.any { - it.cause == ComponentSelectionCause.ROOT - } - } - // endregion - - companion object { - private const val TAG_GROUP_ID = "groupId" - private const val TAG_LICENSES = "licenses" - private const val TAG_LICENSE = "license" - private const val TAG_NAME = "name" - private const val TAG_URL = "url" - - private val knownImportConfiguration = setOf( - "archives", - "debugCompileClasspath", - "debugImplementationDependenciesMetadata", - "debugRuntimeClasspath", - "implementationDependenciesMetadata", - "releaseCompileClasspath", - "releaseImplementationDependenciesMetadata", - "releaseRuntimeClasspath" - ) - private val knownImportTestConfiguration = setOf( - "androidTestImplementationDependenciesMetadata", - "debugAndroidTestCompileClasspath", - "debugAndroidTestImplementationDependenciesMetadata", - "debugAndroidTestRuntimeClasspath", - "debugUnitTestCompileClasspath", - "debugUnitTestImplementationDependenciesMetadata", - "debugUnitTestRuntimeClasspath", - "jacocoAgent", - "jacocoAnt", - "releaseUnitTestCompileClasspath", - "releaseUnitTestImplementationDependenciesMetadata", - "releaseUnitTestRuntimeClasspath", - "testImplementationDependenciesMetadata" - ) - private val knownBuildConfiguration = setOf( - "_internal_aapt2_binary", - "detekt", - "ktlint", - "kotlinCompilerClasspath", - "kotlinCompilerPluginClasspath", - "lintClassPath" - ) - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/License.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/License.kt deleted file mode 100644 index 18f8f43a11..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/License.kt +++ /dev/null @@ -1,47 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -sealed class License { - - data class SPDX(val licenses: List) : License() { - override fun toString(): String { - return licenses.joinToString("/") { it.csvName } - } - } - - data class Raw(val value: String) : License() { - override fun toString(): String { - return "\"$value\"" - } - } - - object Empty : License() { - override fun toString(): String { - return "__" - } - } - - companion object { - fun from(license: String?): License { - val licenseOrEmpty = license.orEmpty() - val matches = - SPDXLicenceConverter.convert( - licenseOrEmpty - ) - return when { - licenseOrEmpty.isEmpty() -> Empty - matches.isNullOrEmpty() -> Raw( - licenseOrEmpty - ) - else -> SPDX( - matches - ) - } - } - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/SPDXLicenceConverter.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/SPDXLicenceConverter.kt deleted file mode 100644 index 1ec00434a1..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/SPDXLicenceConverter.kt +++ /dev/null @@ -1,440 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -import me.xdrop.fuzzywuzzy.FuzzySearch - -object SPDXLicenceConverter { - - fun convert(licenses: String): List? { - val result = licenses.split('/') - .map { - convertLicense( - it - ) - } - - return if (result.isEmpty() || result.contains(null)) { - null - } else { - result.filterNotNull() - } - } - - private fun convertLicense(license: String): SPDXLicense? { - if (license.isBlank()) return null - - val nameMatch = FuzzySearch.extractOne(license.trim(), - nameList - ) - if (nameMatch.score > 90) { - // println("matched name ${nameMatch.score}% [$license] to [${nameMatch.string}]") - return nameMap[nameMatch.string] - } - - val identifierMatch = FuzzySearch.extractOne(license.trim(), - identifierList - ) - if (identifierMatch.score > 90) { - // println("matched identifier ${identifierMatch.score}% [$license] to [${identifierMatch.string}]") - return identifierMap[identifierMatch.string] - } - - // println(" ? no match for [$license]") - // println(" closest name [${nameMatch.string}] ${nameMatch.score}%") - // println(" closest id [${identifierMatch.string}] ${identifierMatch.score}%") - return null - } - - private val identifierMap = SPDXLicense.values() - .map { it.csvName to it } - .toMap() - - private val nameMap = mapOf( - "BSD Zero Clause License" to SPDXLicense._0BSD, - "Attribution Assurance License" to SPDXLicense.AAL, - "Abstyles License" to SPDXLicense.ABSTYLES, - "Adobe Systems Incorporated Source Code License Agreement" to SPDXLicense.ADOBE_2006, - "Adobe Glyph List License" to SPDXLicense.ADOBE_GLYPH, - "Amazon Digital Services License" to SPDXLicense.ADSL, - "Academic Free License v1.1" to SPDXLicense.AFL_1_1, - "Academic Free License v1.2" to SPDXLicense.AFL_1_2, - "Academic Free License v2.0" to SPDXLicense.AFL_2_0, - "Academic Free License v2.1" to SPDXLicense.AFL_2_1, - "Academic Free License v3.0" to SPDXLicense.AFL_3_0, - "Afmparse License" to SPDXLicense.AFMPARSE, - "Affero General Public License v1.0 only" to SPDXLicense.AGPL_1_0_ONLY, - "Affero General Public License v1.0 or later" to SPDXLicense.AGPL_1_0_OR_LATER, - "GNU Affero General Public License v3.0 only" to SPDXLicense.AGPL_3_0_ONLY, - "GNU Affero General Public License v3.0 or later" to SPDXLicense.AGPL_3_0_OR_LATER, - "Aladdin Free Public License" to SPDXLicense.ALADDIN, - "AMD's plpa_map.c License" to SPDXLicense.AMDPLPA, - "Apple MIT License" to SPDXLicense.AML, - "Academy of Motion Picture Arts and Sciences BSD" to SPDXLicense.AMPAS, - "ANTLR Software Rights Notice" to SPDXLicense.ANTLR_PD, - "Apache License 1.0" to SPDXLicense.APACHE_1_0, - "Apache License 1.1" to SPDXLicense.APACHE_1_1, - "Apache License 2.0" to SPDXLicense.APACHE_2_0, - "The Apache License, Version 2.0" to SPDXLicense.APACHE_2_0, - "Adobe Postscript AFM License" to SPDXLicense.APAFML, - "Adaptive Public License 1.0" to SPDXLicense.APL_1_0, - "Apple Public Source License 1.0" to SPDXLicense.APSL_1_0, - "Apple Public Source License 1.1" to SPDXLicense.APSL_1_1, - "Apple Public Source License 1.2" to SPDXLicense.APSL_1_2, - "Apple Public Source License 2.0" to SPDXLicense.APSL_2_0, - "Artistic License 1.0" to SPDXLicense.ARTISTIC_1_0, - "Artistic License 1.0 w/clause 8" to SPDXLicense.ARTISTIC_1_0_CL8, - "Artistic License 1.0 (Perl)" to SPDXLicense.ARTISTIC_1_0_PERL, - "Artistic License 2.0" to SPDXLicense.ARTISTIC_2_0, - "Bahyph License" to SPDXLicense.BAHYPH, - "Barr License" to SPDXLicense.BARR, - "Beerware License" to SPDXLicense.BEERWARE, - "BitTorrent Open Source License v1.0" to SPDXLicense.BITTORRENT_1_0, - "BitTorrent Open Source License v1.1" to SPDXLicense.BITTORRENT_1_1, - "SQLite Blessing" to SPDXLicense.BLESSING, - "Blue Oak Model License 1.0.0" to SPDXLicense.BLUEOAK_1_0_0, - "Borceux license" to SPDXLicense.BORCEUX, - "BSD 1-Clause License" to SPDXLicense.BSD_1_CLAUSE, - "BSD 2-Clause \"Simplified\" License" to SPDXLicense.BSD_2_CLAUSE, - "BSD 2-Clause FreeBSD License" to SPDXLicense.BSD_2_CLAUSE_FREEBSD, - "BSD 2-Clause NetBSD License" to SPDXLicense.BSD_2_CLAUSE_NETBSD, - "BSD-2-Clause Plus Patent License" to SPDXLicense.BSD_2_CLAUSE_PATENT, - "BSD 3-Clause \"New\" or \"Revised\" License" to SPDXLicense.BSD_3_CLAUSE, - "BSD with attribution" to SPDXLicense.BSD_3_CLAUSE_ATTRIBUTION, - "BSD 3-Clause Clear License" to SPDXLicense.BSD_3_CLAUSE_CLEAR, - "Lawrence Berkeley National Labs BSD variant license" to SPDXLicense.BSD_3_CLAUSE_LBNL, - "BSD 3-Clause No Nuclear License" to SPDXLicense.BSD_3_CLAUSE_NO_NUCLEAR_LICENSE, - "BSD 3-Clause No Nuclear License 2014" to SPDXLicense.BSD_3_CLAUSE_NO_NUCLEAR_LICENSE_2014, - "BSD 3-Clause No Nuclear Warranty" to SPDXLicense.BSD_3_CLAUSE_NO_NUCLEAR_WARRANTY, - "BSD 3-Clause Open MPI variant" to SPDXLicense.BSD_3_CLAUSE_OPEN_MPI, - "BSD 4-Clause \"Original\" or \"Old\" License" to SPDXLicense.BSD_4_CLAUSE, - "BSD-4-Clause (University of California-Specific)" to SPDXLicense.BSD_4_CLAUSE_UC, - "BSD Protection License" to SPDXLicense.BSD_PROTECTION, - "BSD Source Code Attribution" to SPDXLicense.BSD_SOURCE_CODE, - "Boost Software License 1.0" to SPDXLicense.BSL_1_0, - "Bouncy Castle Licence" to SPDXLicense.BOUNCY_CASTLE, - "bzip2 and libbzip2 License v1.0.5" to SPDXLicense.BZIP2_1_0_5, - "bzip2 and libbzip2 License v1.0.6" to SPDXLicense.BZIP2_1_0_6, - "Caldera License" to SPDXLicense.CALDERA, - "Computer Associates Trusted Open Source License 1.1" to SPDXLicense.CATOSL_1_1, - "Creative Commons Attribution 1.0 Generic" to SPDXLicense.CC_BY_1_0, - "Creative Commons Attribution 2.0 Generic" to SPDXLicense.CC_BY_2_0, - "Creative Commons Attribution 2.5 Generic" to SPDXLicense.CC_BY_2_5, - "Creative Commons Attribution 3.0 Unported" to SPDXLicense.CC_BY_3_0, - "Creative Commons Attribution 4.0 International" to SPDXLicense.CC_BY_4_0, - "Creative Commons Attribution Non Commercial 1.0 Generic" to SPDXLicense.CC_BY_NC_1_0, - "Creative Commons Attribution Non Commercial 2.0 Generic" to SPDXLicense.CC_BY_NC_2_0, - "Creative Commons Attribution Non Commercial 2.5 Generic" to SPDXLicense.CC_BY_NC_2_5, - "Creative Commons Attribution Non Commercial 3.0 Unported" to SPDXLicense.CC_BY_NC_3_0, - "Creative Commons Attribution Non Commercial 4.0 International" to SPDXLicense.CC_BY_NC_4_0, - "Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic" to SPDXLicense.CC_BY_NC_ND_1_0, - "Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic" to SPDXLicense.CC_BY_NC_ND_2_0, - "Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic" to SPDXLicense.CC_BY_NC_ND_2_5, - "Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported" to SPDXLicense.CC_BY_NC_ND_3_0, - "Creative Commons Attribution Non Commercial No Derivatives 4.0 International" to SPDXLicense.CC_BY_NC_ND_4_0, - "Creative Commons Attribution Non Commercial Share Alike 1.0 Generic" to SPDXLicense.CC_BY_NC_SA_1_0, - "Creative Commons Attribution Non Commercial Share Alike 2.0 Generic" to SPDXLicense.CC_BY_NC_SA_2_0, - "Creative Commons Attribution Non Commercial Share Alike 2.5 Generic" to SPDXLicense.CC_BY_NC_SA_2_5, - "Creative Commons Attribution Non Commercial Share Alike 3.0 Unported" to SPDXLicense.CC_BY_NC_SA_3_0, - "Creative Commons Attribution Non Commercial Share Alike 4.0 International" to SPDXLicense.CC_BY_NC_SA_4_0, - "Creative Commons Attribution No Derivatives 1.0 Generic" to SPDXLicense.CC_BY_ND_1_0, - "Creative Commons Attribution No Derivatives 2.0 Generic" to SPDXLicense.CC_BY_ND_2_0, - "Creative Commons Attribution No Derivatives 2.5 Generic" to SPDXLicense.CC_BY_ND_2_5, - "Creative Commons Attribution No Derivatives 3.0 Unported" to SPDXLicense.CC_BY_ND_3_0, - "Creative Commons Attribution No Derivatives 4.0 International" to SPDXLicense.CC_BY_ND_4_0, - "Creative Commons Attribution Share Alike 1.0 Generic" to SPDXLicense.CC_BY_SA_1_0, - "Creative Commons Attribution Share Alike 2.0 Generic" to SPDXLicense.CC_BY_SA_2_0, - "Creative Commons Attribution Share Alike 2.5 Generic" to SPDXLicense.CC_BY_SA_2_5, - "Creative Commons Attribution Share Alike 3.0 Unported" to SPDXLicense.CC_BY_SA_3_0, - "Creative Commons Attribution Share Alike 4.0 International" to SPDXLicense.CC_BY_SA_4_0, - "Creative Commons Public Domain Dedication and Certification" to SPDXLicense.CC_PDDC, - "Creative Commons Zero v1.0 Universal" to SPDXLicense.CC0_1_0, - "Common Development and Distribution License 1.0" to SPDXLicense.CDDL_1_0, - "Common Development and Distribution License 1.1" to SPDXLicense.CDDL_1_1, - "Community Data License Agreement Permissive 1.0" to SPDXLicense.CDLA_PERMISSIVE_1_0, - "Community Data License Agreement Sharing 1.0" to SPDXLicense.CDLA_SHARING_1_0, - "CeCILL Free Software License Agreement v1.0" to SPDXLicense.CECILL_1_0, - "CeCILL Free Software License Agreement v1.1" to SPDXLicense.CECILL_1_1, - "CeCILL Free Software License Agreement v2.0" to SPDXLicense.CECILL_2_0, - "CeCILL Free Software License Agreement v2.1" to SPDXLicense.CECILL_2_1, - "CeCILL-B Free Software License Agreement" to SPDXLicense.CECILL_B, - "CeCILL-C Free Software License Agreement" to SPDXLicense.CECILL_C, - "CERN Open Hardware Licence v1.1" to SPDXLicense.CERN_OHL_1_1, - "CERN Open Hardware Licence v1.2" to SPDXLicense.CERN_OHL_1_2, - "Clarified Artistic License" to SPDXLicense.CLARTISTIC, - "CNRI Jython License" to SPDXLicense.CNRI_JYTHON, - "CNRI Python License" to SPDXLicense.CNRI_PYTHON, - "CNRI Python Open Source GPL Compatible License Agreement" to SPDXLicense.CNRI_PYTHON_GPL_COMPATIBLE, - "Condor Public License v1.1" to SPDXLicense.CONDOR_1_1, - "copyleft-next 0.3.0" to SPDXLicense.COPYLEFT_NEXT_0_3_0, - "copyleft-next 0.3.1" to SPDXLicense.COPYLEFT_NEXT_0_3_1, - "Common Public Attribution License 1.0" to SPDXLicense.CPAL_1_0, - "Common Public License 1.0" to SPDXLicense.CPL_1_0, - "Code Project Open License 1.02" to SPDXLicense.CPOL_1_02, - "Crossword License" to SPDXLicense.CROSSWORD, - "CrystalStacker License" to SPDXLicense.CRYSTALSTACKER, - "CUA Office Public License v1.0" to SPDXLicense.CUA_OPL_1_0, - "Cube License" to SPDXLicense.CUBE, - "curl License" to SPDXLicense.CURL, - "Deutsche Freie Software Lizenz" to SPDXLicense.D_FSL_1_0, - "diffmark license" to SPDXLicense.DIFFMARK, - "DOC License" to SPDXLicense.DOC, - "Dotseqn License" to SPDXLicense.DOTSEQN, - "DSDP License" to SPDXLicense.DSDP, - "dvipdfm License" to SPDXLicense.DVIPDFM, - "Educational Community License v1.0" to SPDXLicense.ECL_1_0, - "Educational Community License v2.0" to SPDXLicense.ECL_2_0, - "Eiffel Forum License v1.0" to SPDXLicense.EFL_1_0, - "Eiffel Forum License v2.0" to SPDXLicense.EFL_2_0, - "eGenix.com Public License 1.1.0" to SPDXLicense.EGENIX, - "Entessa Public License v1.0" to SPDXLicense.ENTESSA, - "Eclipse Public License 1.0" to SPDXLicense.EPL_1_0, - "Eclipse Public License 2.0" to SPDXLicense.EPL_2_0, - "Erlang Public License v1.1" to SPDXLicense.ERLPL_1_1, - "Etalab Open License 2.0" to SPDXLicense.ETALAB_2_0, - "EU DataGrid Software License" to SPDXLicense.EUDATAGRID, - "European Union Public License 1.0" to SPDXLicense.EUPL_1_0, - "European Union Public License 1.1" to SPDXLicense.EUPL_1_1, - "European Union Public License 1.2" to SPDXLicense.EUPL_1_2, - "Eurosym License" to SPDXLicense.EUROSYM, - "Fair License" to SPDXLicense.FAIR, - "Frameworx Open License 1.0" to SPDXLicense.FRAMEWORX_1_0, - "FreeImage Public License v1.0" to SPDXLicense.FREEIMAGE, - "FSF All Permissive License" to SPDXLicense.FSFAP, - "FSF Unlimited License" to SPDXLicense.FSFUL, - "FSF Unlimited License (with License Retention)" to SPDXLicense.FSFULLR, - "Freetype Project License" to SPDXLicense.FTL, - "GNU Free Documentation License v1.1 only" to SPDXLicense.GFDL_1_1_ONLY, - "GNU Free Documentation License v1.1 or later" to SPDXLicense.GFDL_1_1_OR_LATER, - "GNU Free Documentation License v1.2 only" to SPDXLicense.GFDL_1_2_ONLY, - "GNU Free Documentation License v1.2 or later" to SPDXLicense.GFDL_1_2_OR_LATER, - "GNU Free Documentation License v1.3 only" to SPDXLicense.GFDL_1_3_ONLY, - "GNU Free Documentation License v1.3 or later" to SPDXLicense.GFDL_1_3_OR_LATER, - "Giftware License" to SPDXLicense.GIFTWARE, - "GL2PS License" to SPDXLicense.GL2PS, - "3dfx Glide License" to SPDXLicense.GLIDE, - "Glulxe License" to SPDXLicense.GLULXE, - "gnuplot License" to SPDXLicense.GNUPLOT, - "GNU General Public License v1.0 only" to SPDXLicense.GPL_1_0_ONLY, - "GNU General Public License v1.0 or later" to SPDXLicense.GPL_1_0_OR_LATER, - "GNU General Public License v2.0 only" to SPDXLicense.GPL_2_0_ONLY, - "GNU General Public License v2.0 or later" to SPDXLicense.GPL_2_0_OR_LATER, - "GNU General Public License v3.0 only" to SPDXLicense.GPL_3_0_ONLY, - "GNU General Public License v3.0 or later" to SPDXLicense.GPL_3_0_OR_LATER, - "gSOAP Public License v1.3b" to SPDXLicense.GSOAP_1_3B, - "Haskell Language Report License" to SPDXLicense.HASKELLREPORT, - "Historical Permission Notice and Disclaimer" to SPDXLicense.HPND, - "Historical Permission Notice and Disclaimer - sell variant" to SPDXLicense.HPND_SELL_VARIANT, - "IBM PowerPC Initialization and Boot Software" to SPDXLicense.IBM_PIBS, - "ICU License" to SPDXLicense.ICU, - "Independent JPEG Group License" to SPDXLicense.IJG, - "ImageMagick License" to SPDXLicense.IMAGEMAGICK, - "iMatix Standard Function Library Agreement" to SPDXLicense.IMATIX, - "Imlib2 License" to SPDXLicense.IMLIB2, - "Info-ZIP License" to SPDXLicense.INFO_ZIP, - "Intel Open Source License" to SPDXLicense.INTEL, - "Intel ACPI Software License Agreement" to SPDXLicense.INTEL_ACPI, - "Interbase Public License v1.0" to SPDXLicense.INTERBASE_1_0, - "IPA Font License" to SPDXLicense.IPA, - "IBM Public License v1.0" to SPDXLicense.IPL_1_0, - "ISC License" to SPDXLicense.ISC, - "JasPer License" to SPDXLicense.JASPER_2_0, - "Japan Network Information Center License" to SPDXLicense.JPNIC, - "JSON License" to SPDXLicense.JSON, - "Licence Art Libre 1.2" to SPDXLicense.LAL_1_2, - "Licence Art Libre 1.3" to SPDXLicense.LAL_1_3, - "Latex2e License" to SPDXLicense.LATEX2E, - "Leptonica License" to SPDXLicense.LEPTONICA, - "GNU Library General Public License v2 only" to SPDXLicense.LGPL_2_0_ONLY, - "GNU Library General Public License v2 or later" to SPDXLicense.LGPL_2_0_OR_LATER, - "GNU Lesser General Public License v2.1 only" to SPDXLicense.LGPL_2_1_ONLY, - "GNU Lesser General Public License v2.1 or later" to SPDXLicense.LGPL_2_1_OR_LATER, - "GNU Lesser General Public License v3.0 only" to SPDXLicense.LGPL_3_0_ONLY, - "GNU Lesser General Public License v3.0 or later" to SPDXLicense.LGPL_3_0_OR_LATER, - "Lesser General Public License For Linguistic Resources" to SPDXLicense.LGPLLR, - "libpng License" to SPDXLicense.LIBPNG, - "PNG Reference Library version 2" to SPDXLicense.LIBPNG_2_0, - "libtiff License" to SPDXLicense.LIBTIFF, - "Licence Libre du Québec – Permissive version 1.1" to SPDXLicense.LILIQ_P_1_1, - "Licence Libre du Québec – Réciprocité version 1.1" to SPDXLicense.LILIQ_R_1_1, - "Licence Libre du Québec – Réciprocité forte version 1.1" to SPDXLicense.LILIQ_RPLUS_1_1, - "Linux Kernel Variant of OpenIB.org license" to SPDXLicense.LINUX_OPENIB, - "Lucent Public License Version 1.0" to SPDXLicense.LPL_1_0, - "Lucent Public License v1.02" to SPDXLicense.LPL_1_02, - "LaTeX Project Public License v1.0" to SPDXLicense.LPPL_1_0, - "LaTeX Project Public License v1.1" to SPDXLicense.LPPL_1_1, - "LaTeX Project Public License v1.2" to SPDXLicense.LPPL_1_2, - "LaTeX Project Public License v1.3a" to SPDXLicense.LPPL_1_3A, - "LaTeX Project Public License v1.3c" to SPDXLicense.LPPL_1_3C, - "MakeIndex License" to SPDXLicense.MAKEINDEX, - "The MirOS Licence" to SPDXLicense.MIROS, - "MIT" to SPDXLicense.MIT, - "MIT License" to SPDXLicense.MIT, - "MIT No Attribution" to SPDXLicense.MIT_0, - "Enlightenment License (e16)" to SPDXLicense.MIT_ADVERTISING, - "CMU License" to SPDXLicense.MIT_CMU, - "enna License" to SPDXLicense.MIT_ENNA, - "feh License" to SPDXLicense.MIT_FEH, - "MIT +no-false-attribs license" to SPDXLicense.MITNFA, - "Motosoto License" to SPDXLicense.MOTOSOTO, - "mpich2 License" to SPDXLicense.MPICH2, - "Mozilla Public License 1.0" to SPDXLicense.MPL_1_0, - "Mozilla Public License 1.1" to SPDXLicense.MPL_1_1, - "Mozilla Public License 2.0" to SPDXLicense.MPL_2_0, - "Mozilla Public License 2.0 (no copyleft exception)" to SPDXLicense.MPL_2_0_NO_COPYLEFT_EXCEPTION, - "Microsoft Public License" to SPDXLicense.MS_PL, - "Microsoft Reciprocal License" to SPDXLicense.MS_RL, - "Matrix Template Library License" to SPDXLicense.MTLL, - "Mulan Permissive Software License, Version 1" to SPDXLicense.MULANPSL_1_0, - "Multics License" to SPDXLicense.MULTICS, - "Mup License" to SPDXLicense.MUP, - "NASA Open Source Agreement 1.3" to SPDXLicense.NASA_1_3, - "Naumen Public License" to SPDXLicense.NAUMEN, - "Net Boolean Public License v1" to SPDXLicense.NBPL_1_0, - "University of Illinois/NCSA Open Source License" to SPDXLicense.NCSA, - "Net-SNMP License" to SPDXLicense.NET_SNMP, - "NetCDF license" to SPDXLicense.NETCDF, - "Newsletr License" to SPDXLicense.NEWSLETR, - "Nethack General Public License" to SPDXLicense.NGPL, - "Norwegian Licence for Open Government Data" to SPDXLicense.NLOD_1_0, - "No Limit Public License" to SPDXLicense.NLPL, - "Nokia Open Source License" to SPDXLicense.NOKIA, - "Netizen Open Source License" to SPDXLicense.NOSL, - "Noweb License" to SPDXLicense.NOWEB, - "Netscape Public License v1.0" to SPDXLicense.NPL_1_0, - "Netscape Public License v1.1" to SPDXLicense.NPL_1_1, - "Non-Profit Open Software License 3.0" to SPDXLicense.NPOSL_3_0, - "NRL License" to SPDXLicense.NRL, - "NTP License" to SPDXLicense.NTP, - "Open CASCADE Technology Public License" to SPDXLicense.OCCT_PL, - "OCLC Research Public License 2.0" to SPDXLicense.OCLC_2_0, - "ODC Open Database License v1.0" to SPDXLicense.ODBL_1_0, - "Open Data Commons Attribution License v1.0" to SPDXLicense.ODC_BY_1_0, - "SIL Open Font License 1.0" to SPDXLicense.OFL_1_0, - "SIL Open Font License 1.1" to SPDXLicense.OFL_1_1, - "Open Government Licence - Canada" to SPDXLicense.OGL_CANADA_2_0, - "Open Government Licence v1.0" to SPDXLicense.OGL_UK_1_0, - "Open Government Licence v2.0" to SPDXLicense.OGL_UK_2_0, - "Open Government Licence v3.0" to SPDXLicense.OGL_UK_3_0, - "Open Group Test Suite License" to SPDXLicense.OGTSL, - "Open LDAP Public License v1.1" to SPDXLicense.OLDAP_1_1, - "Open LDAP Public License v1.2" to SPDXLicense.OLDAP_1_2, - "Open LDAP Public License v1.3" to SPDXLicense.OLDAP_1_3, - "Open LDAP Public License v1.4" to SPDXLicense.OLDAP_1_4, - "Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B)" to SPDXLicense.OLDAP_2_0, - "Open LDAP Public License v2.0.1" to SPDXLicense.OLDAP_2_0_1, - "Open LDAP Public License v2.1" to SPDXLicense.OLDAP_2_1, - "Open LDAP Public License v2.2" to SPDXLicense.OLDAP_2_2, - "Open LDAP Public License v2.2.1" to SPDXLicense.OLDAP_2_2_1, - "Open LDAP Public License 2.2.2" to SPDXLicense.OLDAP_2_2_2, - "Open LDAP Public License v2.3" to SPDXLicense.OLDAP_2_3, - "Open LDAP Public License v2.4" to SPDXLicense.OLDAP_2_4, - "Open LDAP Public License v2.5" to SPDXLicense.OLDAP_2_5, - "Open LDAP Public License v2.6" to SPDXLicense.OLDAP_2_6, - "Open LDAP Public License v2.7" to SPDXLicense.OLDAP_2_7, - "Open LDAP Public License v2.8" to SPDXLicense.OLDAP_2_8, - "Open Market License" to SPDXLicense.OML, - "OpenSSL License" to SPDXLicense.OPENSSL, - "Open Public License v1.0" to SPDXLicense.OPL_1_0, - "OSET Public License version 2.1" to SPDXLicense.OSET_PL_2_1, - "Open Software License 1.0" to SPDXLicense.OSL_1_0, - "Open Software License 1.1" to SPDXLicense.OSL_1_1, - "Open Software License 2.0" to SPDXLicense.OSL_2_0, - "Open Software License 2.1" to SPDXLicense.OSL_2_1, - "Open Software License 3.0" to SPDXLicense.OSL_3_0, - "The Parity Public License 6.0.0" to SPDXLicense.PARITY_6_0_0, - "ODC Public Domain Dedication & License 1.0" to SPDXLicense.PDDL_1_0, - "PHP License v3.0" to SPDXLicense.PHP_3_0, - "PHP License v3.01" to SPDXLicense.PHP_3_01, - "Plexus Classworlds License" to SPDXLicense.PLEXUS, - "PostgreSQL License" to SPDXLicense.POSTGRESQL, - "psfrag License" to SPDXLicense.PSFRAG, - "psutils License" to SPDXLicense.PSUTILS, - "Public Domain" to SPDXLicense.PUBLIC_DOMAIN, - "Python License 2.0" to SPDXLicense.PYTHON_2_0, - "Qhull License" to SPDXLicense.QHULL, - "Q Public License 1.0" to SPDXLicense.QPL_1_0, - "Rdisc License" to SPDXLicense.RDISC, - "Red Hat eCos Public License v1.1" to SPDXLicense.RHECOS_1_1, - "Reciprocal Public License 1.1" to SPDXLicense.RPL_1_1, - "Reciprocal Public License 1.5" to SPDXLicense.RPL_1_5, - "RealNetworks Public Source License v1.0" to SPDXLicense.RPSL_1_0, - "RSA Message-Digest License" to SPDXLicense.RSA_MD, - "Ricoh Source Code Public License" to SPDXLicense.RSCPL, - "Ruby License" to SPDXLicense.RUBY, - "Sax Public Domain Notice" to SPDXLicense.SAX_PD, - "Saxpath License" to SPDXLicense.SAXPATH, - "SCEA Shared Source License" to SPDXLicense.SCEA, - "Sendmail License" to SPDXLicense.SENDMAIL, - "Sendmail License 8.23" to SPDXLicense.SENDMAIL_8_23, - "SGI Free Software License B v1.0" to SPDXLicense.SGI_B_1_0, - "SGI Free Software License B v1.1" to SPDXLicense.SGI_B_1_1, - "SGI Free Software License B v2.0" to SPDXLicense.SGI_B_2_0, - "Solderpad Hardware License v0.5" to SPDXLicense.SHL_0_5, - "Solderpad Hardware License, Version 0.51" to SPDXLicense.SHL_0_51, - "Simple Public License 2.0" to SPDXLicense.SIMPL_2_0, - "Sun Industry Standards Source License v1.1" to SPDXLicense.SISSL, - "Sun Industry Standards Source License v1.2" to SPDXLicense.SISSL_1_2, - "Sleepycat License" to SPDXLicense.SLEEPYCAT, - "Standard ML of New Jersey License" to SPDXLicense.SMLNJ, - "Secure Messaging Protocol Public License" to SPDXLicense.SMPPL, - "SNIA Public License 1.1" to SPDXLicense.SNIA, - "Spencer License 86" to SPDXLicense.SPENCER_86, - "Spencer License 94" to SPDXLicense.SPENCER_94, - "Spencer License 99" to SPDXLicense.SPENCER_99, - "Sun Public License v1.0" to SPDXLicense.SPL_1_0, - "SSH OpenSSH license" to SPDXLicense.SSH_OPENSSH, - "SSH short notice" to SPDXLicense.SSH_SHORT, - "Server Side Public License, v 1" to SPDXLicense.SSPL_1_0, - "SugarCRM Public License v1.1.3" to SPDXLicense.SUGARCRM_1_1_3, - "Scheme Widget Library (SWL) Software License Agreement" to SPDXLicense.SWL, - "TAPR Open Hardware License v1.0" to SPDXLicense.TAPR_OHL_1_0, - "TCL/TK License" to SPDXLicense.TCL, - "TCP Wrappers License" to SPDXLicense.TCP_WRAPPERS, - "TMate Open Source License" to SPDXLicense.TMATE, - "TORQUE v2.5+ Software License v1.1" to SPDXLicense.TORQUE_1_1, - "Trusster Open Source License" to SPDXLicense.TOSL, - "Technische Universitaet Berlin License 1.0" to SPDXLicense.TU_BERLIN_1_0, - "Technische Universitaet Berlin License 2.0" to SPDXLicense.TU_BERLIN_2_0, - "Upstream Compatibility License v1.0" to SPDXLicense.UCL_1_0, - "Unicode License Agreement - Data Files and Software (2015)" to SPDXLicense.UNICODE_DFS_2015, - "Unicode License Agreement - Data Files and Software (2016)" to SPDXLicense.UNICODE_DFS_2016, - "Unicode Terms of Use" to SPDXLicense.UNICODE_TOU, - "The Unlicense" to SPDXLicense.UNLICENSE, - "Universal Permissive License v1.0" to SPDXLicense.UPL_1_0, - "Vim License" to SPDXLicense.VIM, - "VOSTROM Public License for Open Source" to SPDXLicense.VOSTROM, - "Vovida Software License v1.0" to SPDXLicense.VSL_1_0, - "W3C Software Notice and License (2002-12-31)" to SPDXLicense.W3C, - "W3C Software Notice and License (1998-07-20)" to SPDXLicense.W3C_19980720, - "W3C Software Notice and Document License (2015-05-13)" to SPDXLicense.W3C_20150513, - "Sybase Open Watcom Public License 1.0" to SPDXLicense.WATCOM_1_0, - "Wsuipa License" to SPDXLicense.WSUIPA, - "Do What The F*ck You Want To Public License" to SPDXLicense.WTFPL, - "X11 License" to SPDXLicense.X11, - "Xerox License" to SPDXLicense.XEROX, - "XFree86 License 1.1" to SPDXLicense.XFREE86_1_1, - "xinetd License" to SPDXLicense.XINETD, - "X.Net License" to SPDXLicense.XNET, - "XPP License" to SPDXLicense.XPP, - "XSkat License" to SPDXLicense.XSKAT, - "Yahoo! Public License v1.0" to SPDXLicense.YPL_1_0, - "Yahoo! Public License v1.1" to SPDXLicense.YPL_1_1, - "Zed License" to SPDXLicense.ZED, - "Zend License v2.0" to SPDXLicense.ZEND_2_0, - "Zimbra Public License v1.3" to SPDXLicense.ZIMBRA_1_3, - "Zimbra Public License v1.4" to SPDXLicense.ZIMBRA_1_4, - "zlib License" to SPDXLicense.ZLIB, - "zlib/libpng License with Acknowledgement" to SPDXLicense.ZLIB_ACKNOWLEDGEMENT, - "Zope Public License 1.1" to SPDXLicense.ZPL_1_1, - "Zope Public License 2.0" to SPDXLicense.ZPL_2_0, - "Zope Public License 2.1" to SPDXLicense.ZPL_2_1 - ) - - private val nameList = nameMap.keys - private val identifierList = nameMap.values.map { it.csvName } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/SPDXLicense.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/SPDXLicense.kt deleted file mode 100644 index 00b10ec2d7..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/SPDXLicense.kt +++ /dev/null @@ -1,386 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -enum class SPDXLicense(val csvName: String) { - _0BSD("0BSD"), - AAL("AAL"), - ABSTYLES("Abstyles"), - ADOBE_2006("Adobe-2006"), - ADOBE_GLYPH("Adobe-Glyph"), - ADSL("ADSL"), - AFL_1_1("AFL-1.1"), - AFL_1_2("AFL-1.2"), - AFL_2_0("AFL-2.0"), - AFL_2_1("AFL-2.1"), - AFL_3_0("AFL-3.0"), - AFMPARSE("Afmparse"), - AGPL_1_0_ONLY("AGPL-1.0-only"), - AGPL_1_0_OR_LATER("AGPL-1.0-or-later"), - AGPL_3_0_ONLY("AGPL-3.0-only"), - AGPL_3_0_OR_LATER("AGPL-3.0-or-later"), - ALADDIN("Aladdin"), - AMDPLPA("AMDPLPA"), - AML("AML"), - AMPAS("AMPAS"), - ANTLR_PD("ANTLR-PD"), - APACHE_1_0("Apache-1.0"), - APACHE_1_1("Apache-1.1"), - APACHE_2_0("Apache-2.0"), - APAFML("APAFML"), - APL_1_0("APL-1.0"), - APSL_1_0("APSL-1.0"), - APSL_1_1("APSL-1.1"), - APSL_1_2("APSL-1.2"), - APSL_2_0("APSL-2.0"), - ARTISTIC_1_0("Artistic-1.0"), - ARTISTIC_1_0_CL8("Artistic-1.0-cl8"), - ARTISTIC_1_0_PERL("Artistic-1.0-Perl"), - ARTISTIC_2_0("Artistic-2.0"), - BAHYPH("Bahyph"), - BARR("Barr"), - BEERWARE("Beerware"), - BITTORRENT_1_0("BitTorrent-1.0"), - BITTORRENT_1_1("BitTorrent-1.1"), - BLESSING("blessing"), - BLUEOAK_1_0_0("BlueOak-1.0.0"), - BORCEUX("Borceux"), - BSD_1_CLAUSE("BSD-1-Clause"), - BSD_2_CLAUSE("BSD-2-Clause"), - BSD_2_CLAUSE_FREEBSD("BSD-2-Clause-FreeBSD"), - BSD_2_CLAUSE_NETBSD("BSD-2-Clause-NetBSD"), - BSD_2_CLAUSE_PATENT("BSD-2-Clause-Patent"), - BSD_3_CLAUSE("BSD-3-Clause"), - BSD_3_CLAUSE_ATTRIBUTION("BSD-3-Clause-Attribution"), - BSD_3_CLAUSE_CLEAR("BSD-3-Clause-Clear"), - BSD_3_CLAUSE_LBNL("BSD-3-Clause-LBNL"), - BSD_3_CLAUSE_NO_NUCLEAR_LICENSE("BSD-3-Clause-No-Nuclear-License"), - BSD_3_CLAUSE_NO_NUCLEAR_LICENSE_2014("BSD-3-Clause-No-Nuclear-License-2014"), - BSD_3_CLAUSE_NO_NUCLEAR_WARRANTY("BSD-3-Clause-No-Nuclear-Warranty"), - BSD_3_CLAUSE_OPEN_MPI("BSD-3-Clause-Open-MPI"), - BSD_4_CLAUSE("BSD-4-Clause"), - BSD_4_CLAUSE_UC("BSD-4-Clause-UC"), - BSD_PROTECTION("BSD-Protection"), - BSD_SOURCE_CODE("BSD-Source-Code"), - BSL_1_0("BSL-1.0"), - BOUNCY_CASTLE("BouncyCastle-License"), - BZIP2_1_0_5("bzip2-1.0.5"), - BZIP2_1_0_6("bzip2-1.0.6"), - CALDERA("Caldera"), - CATOSL_1_1("CATOSL-1.1"), - CC_BY_1_0("CC-BY-1.0"), - CC_BY_2_0("CC-BY-2.0"), - CC_BY_2_5("CC-BY-2.5"), - CC_BY_3_0("CC-BY-3.0"), - CC_BY_4_0("CC-BY-4.0"), - CC_BY_NC_1_0("CC-BY-NC-1.0"), - CC_BY_NC_2_0("CC-BY-NC-2.0"), - CC_BY_NC_2_5("CC-BY-NC-2.5"), - CC_BY_NC_3_0("CC-BY-NC-3.0"), - CC_BY_NC_4_0("CC-BY-NC-4.0"), - CC_BY_NC_ND_1_0("CC-BY-NC-ND-1.0"), - CC_BY_NC_ND_2_0("CC-BY-NC-ND-2.0"), - CC_BY_NC_ND_2_5("CC-BY-NC-ND-2.5"), - CC_BY_NC_ND_3_0("CC-BY-NC-ND-3.0"), - CC_BY_NC_ND_4_0("CC-BY-NC-ND-4.0"), - CC_BY_NC_SA_1_0("CC-BY-NC-SA-1.0"), - CC_BY_NC_SA_2_0("CC-BY-NC-SA-2.0"), - CC_BY_NC_SA_2_5("CC-BY-NC-SA-2.5"), - CC_BY_NC_SA_3_0("CC-BY-NC-SA-3.0"), - CC_BY_NC_SA_4_0("CC-BY-NC-SA-4.0"), - CC_BY_ND_1_0("CC-BY-ND-1.0"), - CC_BY_ND_2_0("CC-BY-ND-2.0"), - CC_BY_ND_2_5("CC-BY-ND-2.5"), - CC_BY_ND_3_0("CC-BY-ND-3.0"), - CC_BY_ND_4_0("CC-BY-ND-4.0"), - CC_BY_SA_1_0("CC-BY-SA-1.0"), - CC_BY_SA_2_0("CC-BY-SA-2.0"), - CC_BY_SA_2_5("CC-BY-SA-2.5"), - CC_BY_SA_3_0("CC-BY-SA-3.0"), - CC_BY_SA_4_0("CC-BY-SA-4.0"), - CC_PDDC("CC-PDDC"), - CC0_1_0("CC0-1.0"), - CDDL_1_0("CDDL-1.0"), - CDDL_1_1("CDDL-1.1"), - CDLA_PERMISSIVE_1_0("CDLA-Permissive-1.0"), - CDLA_SHARING_1_0("CDLA-Sharing-1.0"), - CECILL_1_0("CECILL-1.0"), - CECILL_1_1("CECILL-1.1"), - CECILL_2_0("CECILL-2.0"), - CECILL_2_1("CECILL-2.1"), - CECILL_B("CECILL-B"), - CECILL_C("CECILL-C"), - CERN_OHL_1_1("CERN-OHL-1.1"), - CERN_OHL_1_2("CERN-OHL-1.2"), - CLARTISTIC("ClArtistic"), - CNRI_JYTHON("CNRI-Jython"), - CNRI_PYTHON("CNRI-Python"), - CNRI_PYTHON_GPL_COMPATIBLE("CNRI-Python-GPL-Compatible"), - CONDOR_1_1("Condor-1.1"), - COPYLEFT_NEXT_0_3_0("copyleft-next-0.3.0"), - COPYLEFT_NEXT_0_3_1("copyleft-next-0.3.1"), - CPAL_1_0("CPAL-1.0"), - CPL_1_0("CPL-1.0"), - CPOL_1_02("CPOL-1.02"), - CROSSWORD("Crossword"), - CRYSTALSTACKER("CrystalStacker"), - CUA_OPL_1_0("CUA-OPL-1.0"), - CUBE("Cube"), - CURL("curl"), - D_FSL_1_0("D-FSL-1.0"), - DIFFMARK("diffmark"), - DOC("DOC"), - DOTSEQN("Dotseqn"), - DSDP("DSDP"), - DVIPDFM("dvipdfm"), - ECL_1_0("ECL-1.0"), - ECL_2_0("ECL-2.0"), - EFL_1_0("EFL-1.0"), - EFL_2_0("EFL-2.0"), - EGENIX("eGenix"), - ENTESSA("Entessa"), - EPL_1_0("EPL-1.0"), - EPL_2_0("EPL-2.0"), - ERLPL_1_1("ErlPL-1.1"), - ETALAB_2_0("etalab-2.0"), - EUDATAGRID("EUDatagrid"), - EUPL_1_0("EUPL-1.0"), - EUPL_1_1("EUPL-1.1"), - EUPL_1_2("EUPL-1.2"), - EUROSYM("Eurosym"), - FAIR("Fair"), - FRAMEWORX_1_0("Frameworx-1.0"), - FREEIMAGE("FreeImage"), - FSFAP("FSFAP"), - FSFUL("FSFUL"), - FSFULLR("FSFULLR"), - FTL("FTL"), - GFDL_1_1_ONLY("GFDL-1.1-only"), - GFDL_1_1_OR_LATER("GFDL-1.1-or-later"), - GFDL_1_2_ONLY("GFDL-1.2-only"), - GFDL_1_2_OR_LATER("GFDL-1.2-or-later"), - GFDL_1_3_ONLY("GFDL-1.3-only"), - GFDL_1_3_OR_LATER("GFDL-1.3-or-later"), - GIFTWARE("Giftware"), - GL2PS("GL2PS"), - GLIDE("Glide"), - GLULXE("Glulxe"), - GNUPLOT("gnuplot"), - GPL_1_0_ONLY("GPL-1.0-only"), - GPL_1_0_OR_LATER("GPL-1.0-or-later"), - GPL_2_0_ONLY("GPL-2.0-only"), - GPL_2_0_OR_LATER("GPL-2.0-or-later"), - GPL_3_0_ONLY("GPL-3.0-only"), - GPL_3_0_OR_LATER("GPL-3.0-or-later"), - GSOAP_1_3B("gSOAP-1.3b"), - HASKELLREPORT("HaskellReport"), - HPND("HPND"), - HPND_SELL_VARIANT("HPND-sell-variant"), - IBM_PIBS("IBM-pibs"), - ICU("ICU"), - IJG("IJG"), - IMAGEMAGICK("ImageMagick"), - IMATIX("iMatix"), - IMLIB2("Imlib2"), - INFO_ZIP("Info-ZIP"), - INTEL("Intel"), - INTEL_ACPI("Intel-ACPI"), - INTERBASE_1_0("Interbase-1.0"), - IPA("IPA"), - IPL_1_0("IPL-1.0"), - ISC("ISC"), - JASPER_2_0("JasPer-2.0"), - JPNIC("JPNIC"), - JSON("JSON"), - LAL_1_2("LAL-1.2"), - LAL_1_3("LAL-1.3"), - LATEX2E("Latex2e"), - LEPTONICA("Leptonica"), - LGPL_2_0_ONLY("LGPL-2.0-only"), - LGPL_2_0_OR_LATER("LGPL-2.0-or-later"), - LGPL_2_1_ONLY("LGPL-2.1-only"), - LGPL_2_1_OR_LATER("LGPL-2.1-or-later"), - LGPL_3_0_ONLY("LGPL-3.0-only"), - LGPL_3_0_OR_LATER("LGPL-3.0-or-later"), - LGPLLR("LGPLLR"), - LIBPNG("Libpng"), - LIBPNG_2_0("libpng-2.0"), - LIBTIFF("libtiff"), - LILIQ_P_1_1("LiLiQ-P-1.1"), - LILIQ_R_1_1("LiLiQ-R-1.1"), - LILIQ_RPLUS_1_1("LiLiQ-Rplus-1.1"), - LINUX_OPENIB("Linux-OpenIB"), - LPL_1_0("LPL-1.0"), - LPL_1_02("LPL-1.02"), - LPPL_1_0("LPPL-1.0"), - LPPL_1_1("LPPL-1.1"), - LPPL_1_2("LPPL-1.2"), - LPPL_1_3A("LPPL-1.3a"), - LPPL_1_3C("LPPL-1.3c"), - MAKEINDEX("MakeIndex"), - MIROS("MirOS"), - MIT("MIT"), - MIT_0("MIT-0"), - MIT_ADVERTISING("MIT-advertising"), - MIT_CMU("MIT-CMU"), - MIT_ENNA("MIT-enna"), - MIT_FEH("MIT-feh"), - MITNFA("MITNFA"), - MOTOSOTO("Motosoto"), - MPICH2("mpich2"), - MPL_1_0("MPL-1.0"), - MPL_1_1("MPL-1.1"), - MPL_2_0("MPL-2.0"), - MPL_2_0_NO_COPYLEFT_EXCEPTION("MPL-2.0-no-copyleft-exception"), - MS_PL("MS-PL"), - MS_RL("MS-RL"), - MTLL("MTLL"), - MULANPSL_1_0("MulanPSL-1.0"), - MULTICS("Multics"), - MUP("Mup"), - NASA_1_3("NASA-1.3"), - NAUMEN("Naumen"), - NBPL_1_0("NBPL-1.0"), - NCSA("NCSA"), - NET_SNMP("Net-SNMP"), - NETCDF("NetCDF"), - NEWSLETR("Newsletr"), - NGPL("NGPL"), - NLOD_1_0("NLOD-1.0"), - NLPL("NLPL"), - NOKIA("Nokia"), - NOSL("NOSL"), - NOWEB("Noweb"), - NPL_1_0("NPL-1.0"), - NPL_1_1("NPL-1.1"), - NPOSL_3_0("NPOSL-3.0"), - NRL("NRL"), - NTP("NTP"), - OCCT_PL("OCCT-PL"), - OCLC_2_0("OCLC-2.0"), - ODBL_1_0("ODbL-1.0"), - ODC_BY_1_0("ODC-By-1.0"), - OFL_1_0("OFL-1.0"), - OFL_1_1("OFL-1.1"), - OGL_CANADA_2_0("OGL-Canada-2.0"), - OGL_UK_1_0("OGL-UK-1.0"), - OGL_UK_2_0("OGL-UK-2.0"), - OGL_UK_3_0("OGL-UK-3.0"), - OGTSL("OGTSL"), - OLDAP_1_1("OLDAP-1.1"), - OLDAP_1_2("OLDAP-1.2"), - OLDAP_1_3("OLDAP-1.3"), - OLDAP_1_4("OLDAP-1.4"), - OLDAP_2_0("OLDAP-2.0"), - OLDAP_2_0_1("OLDAP-2.0.1"), - OLDAP_2_1("OLDAP-2.1"), - OLDAP_2_2("OLDAP-2.2"), - OLDAP_2_2_1("OLDAP-2.2.1"), - OLDAP_2_2_2("OLDAP-2.2.2"), - OLDAP_2_3("OLDAP-2.3"), - OLDAP_2_4("OLDAP-2.4"), - OLDAP_2_5("OLDAP-2.5"), - OLDAP_2_6("OLDAP-2.6"), - OLDAP_2_7("OLDAP-2.7"), - OLDAP_2_8("OLDAP-2.8"), - OML("OML"), - OPENSSL("OpenSSL"), - OPL_1_0("OPL-1.0"), - OSET_PL_2_1("OSET-PL-2.1"), - OSL_1_0("OSL-1.0"), - OSL_1_1("OSL-1.1"), - OSL_2_0("OSL-2.0"), - OSL_2_1("OSL-2.1"), - OSL_3_0("OSL-3.0"), - PARITY_6_0_0("Parity-6.0.0"), - PDDL_1_0("PDDL-1.0"), - PHP_3_0("PHP-3.0"), - PHP_3_01("PHP-3.01"), - PLEXUS("Plexus"), - POSTGRESQL("PostgreSQL"), - PSFRAG("psfrag"), - PSUTILS("psutils"), - PUBLIC_DOMAIN("Public Domain"), - PYTHON_2_0("Python-2.0"), - QHULL("Qhull"), - QPL_1_0("QPL-1.0"), - RDISC("Rdisc"), - RHECOS_1_1("RHeCos-1.1"), - RPL_1_1("RPL-1.1"), - RPL_1_5("RPL-1.5"), - RPSL_1_0("RPSL-1.0"), - RSA_MD("RSA-MD"), - RSCPL("RSCPL"), - RUBY("Ruby"), - SAX_PD("SAX-PD"), - SAXPATH("Saxpath"), - SCEA("SCEA"), - SENDMAIL("Sendmail"), - SENDMAIL_8_23("Sendmail-8.23"), - SGI_B_1_0("SGI-B-1.0"), - SGI_B_1_1("SGI-B-1.1"), - SGI_B_2_0("SGI-B-2.0"), - SHL_0_5("SHL-0.5"), - SHL_0_51("SHL-0.51"), - SIMPL_2_0("SimPL-2.0"), - SISSL("SISSL"), - SISSL_1_2("SISSL-1.2"), - SLEEPYCAT("Sleepycat"), - SMLNJ("SMLNJ"), - SMPPL("SMPPL"), - SNIA("SNIA"), - SPENCER_86("Spencer-86"), - SPENCER_94("Spencer-94"), - SPENCER_99("Spencer-99"), - SPL_1_0("SPL-1.0"), - SSH_OPENSSH("SSH-OpenSSH"), - SSH_SHORT("SSH-short"), - SSPL_1_0("SSPL-1.0"), - SUGARCRM_1_1_3("SugarCRM-1.1.3"), - SWL("SWL"), - TAPR_OHL_1_0("TAPR-OHL-1.0"), - TCL("TCL"), - TCP_WRAPPERS("TCP-wrappers"), - TMATE("TMate"), - TORQUE_1_1("TORQUE-1.1"), - TOSL("TOSL"), - TU_BERLIN_1_0("TU-Berlin-1.0"), - TU_BERLIN_2_0("TU-Berlin-2.0"), - UCL_1_0("UCL-1.0"), - UNICODE_DFS_2015("Unicode-DFS-2015"), - UNICODE_DFS_2016("Unicode-DFS-2016"), - UNICODE_TOU("Unicode-TOU"), - UNLICENSE("Unlicense"), - UPL_1_0("UPL-1.0"), - VIM("Vim"), - VOSTROM("VOSTROM"), - VSL_1_0("VSL-1.0"), - W3C("W3C"), - W3C_19980720("W3C-19980720"), - W3C_20150513("W3C-20150513"), - WATCOM_1_0("Watcom-1.0"), - WSUIPA("Wsuipa"), - WTFPL("WTFPL"), - X11("X11"), - XEROX("Xerox"), - XFREE86_1_1("XFree86-1.1"), - XINETD("xinetd"), - XNET("Xnet"), - XPP("xpp"), - XSKAT("XSkat"), - YPL_1_0("YPL-1.0"), - YPL_1_1("YPL-1.1"), - ZED("Zed"), - ZEND_2_0("Zend-2.0"), - ZIMBRA_1_3("Zimbra-1.3"), - ZIMBRA_1_4("Zimbra-1.4"), - ZLIB("Zlib"), - ZLIB_ACKNOWLEDGEMENT("zlib-acknowledgement"), - ZPL_1_1("ZPL-1.1"), - ZPL_2_0("ZPL-2.0"), - ZPL_2_1("ZPL-2.1") -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyDependency.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyDependency.kt deleted file mode 100644 index 7ea8052db3..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyDependency.kt +++ /dev/null @@ -1,25 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -data class ThirdPartyDependency( - val component: Component, - val origin: String, - val license: License, - val copyright: String -) { - enum class Component(val csvName: String) { - IMPORT("import"), - IMPORT_TEST("import(test)"), - BUILD("build"), - UNKNOWN("__") - } - - override fun toString(): String { - return "${component.csvName},$origin,$license,__" - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyLicensesExtension.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyLicensesExtension.kt deleted file mode 100644 index 79e33900ba..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyLicensesExtension.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -import java.io.File - -open class ThirdPartyLicensesExtension( - var csvFile: File = File(DEFAULT_TP_LICENCE_FILENAME), - var listDependencyOnce: Boolean = true, - var transitiveDependencies: Boolean = false, - var checkObsoleteDependencies: Boolean = false -) { - companion object { - const val DEFAULT_TP_LICENCE_FILENAME = "LICENSE-3rdparty.csv" - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyLicensesPlugin.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyLicensesPlugin.kt deleted file mode 100644 index a2520d524a..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/ThirdPartyLicensesPlugin.kt +++ /dev/null @@ -1,40 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -import com.android.build.gradle.internal.tasks.factory.dependsOn -import java.io.File -import org.gradle.api.Plugin -import org.gradle.api.Project - -class ThirdPartyLicensesPlugin : Plugin { - - override fun apply(target: Project) { - val extension = target.extensions - .create(EXT_NAME, ThirdPartyLicensesExtension::class.java) - extension.csvFile = File(target.rootDir, - ThirdPartyLicensesExtension.DEFAULT_TP_LICENCE_FILENAME - ) - - val updateTask = target.tasks - .create(TASK_UPDATE_NAME, UpdateThirdPartyLicensesTask::class.java) - updateTask.extension = extension - - val checkTask = target.tasks - .create(TASK_CHECK_NAME, CheckThirdPartyLicensesTask::class.java) - checkTask.extension = extension - - target.tasks.named("check").dependsOn(TASK_CHECK_NAME) - } - - companion object { - const val EXT_NAME = "thirdPartyLicences" - - const val TASK_UPDATE_NAME = "updateThirdPartyLicences" - const val TASK_CHECK_NAME = "checkThirdPartyLicences" - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/UpdateThirdPartyLicensesTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/UpdateThirdPartyLicensesTask.kt deleted file mode 100644 index a0fbbab566..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/checklicenses/UpdateThirdPartyLicensesTask.kt +++ /dev/null @@ -1,46 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.checklicenses - -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.TaskAction - -open class UpdateThirdPartyLicensesTask : DefaultTask() { - - @get: Input - internal var extension: ThirdPartyLicensesExtension = - ThirdPartyLicensesExtension() - private val provider: DependenciesLicenseProvider = - DependenciesLicenseProvider() - - init { - group = "datadog" - description = "Lists Third Party Licences in a csv file" - } - - // region Task - - @TaskAction - fun applyTask() { - val dependencies = provider.getThirdPartyDependencies( - project, - extension.transitiveDependencies, - extension.listDependencyOnce - ) - - extension.csvFile.printWriter().use { writer -> - writer.println("Component,Origin,License,Copyright") - dependencies - .forEach { - writer.println(it.toString()) - } - } - } - - // endregion -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesExtension.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesExtension.kt index a270734cb9..e23d433cd8 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesExtension.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesExtension.kt @@ -16,7 +16,11 @@ open class GitCloneDependenciesExtension : Serializable { var excludedPrefixes: List, var originRef: String, var destinationFolder: String - ) : Serializable + ) : Serializable { + companion object { + private const val serialVersionUID: Long = 1L + } + } internal val dependencies: MutableList = mutableListOf() @@ -31,4 +35,8 @@ open class GitCloneDependenciesExtension : Serializable { Dependency(repo, subFolder, excludedPrefixes, ref, destinationFolder) ) } + + companion object { + private const val serialVersionUID: Long = 1L + } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesPlugin.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesPlugin.kt index 2f15875658..a06c5c7d48 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesPlugin.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesPlugin.kt @@ -15,9 +15,10 @@ class GitCloneDependenciesPlugin : Plugin { val extension = target.extensions .create(EXT_NAME, GitCloneDependenciesExtension::class.java) - val cloneTask = target.tasks - .create(TASK_NAME, GitCloneDependenciesTask::class.java) - cloneTask.extension = extension + target.tasks + .register(TASK_NAME, GitCloneDependenciesTask::class.java) { + this.extension = extension + } } companion object { diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesTask.kt index b1a46f9614..34fabc5a00 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesTask.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/gitclone/GitCloneDependenciesTask.kt @@ -7,12 +7,17 @@ package com.datadog.gradle.plugin.gitclone import com.datadog.gradle.utils.execShell -import java.io.File import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import java.io.File +import java.nio.file.Files.createTempDirectory +import javax.inject.Inject -open class GitCloneDependenciesTask : DefaultTask() { +open class GitCloneDependenciesTask @Inject constructor( + private val execOperations: ExecOperations +) : DefaultTask() { @get: Input var extension: GitCloneDependenciesExtension = @@ -40,7 +45,7 @@ open class GitCloneDependenciesTask : DefaultTask() { private fun cloneDependency( dependency: GitCloneDependenciesExtension.Dependency ) { - val target = createTempDir() + val target = createTempDirectory(null).toFile() cloneRepository(dependency, target) val copyFrom = if (dependency.originSubFolder.isEmpty()) { @@ -59,10 +64,13 @@ open class GitCloneDependenciesTask : DefaultTask() { target: File ) { println(" --- Cloning ${dependency.originRepository} into ${target.absolutePath}") - project.execShell( - "git", "clone", - "--branch", dependency.originRef, - "--depth", "1", + execOperations.execShell( + "git", + "clone", + "--branch", + dependency.originRef, + "--depth", + "1", dependency.originRepository, target.absolutePath ) @@ -121,21 +129,17 @@ open class GitCloneDependenciesTask : DefaultTask() { } private fun keepLine(line: String): Boolean { - if (LOG_LINE_REGEX.matches(line)) { - return false - } - if (SLF4J_REGEX.matches(line)) { - return false - } - if (LOMBOK_IMPORTS_REGEX.matches(line)) { - return false + return when { + LOG_LINE_REGEX.matches(line) -> false + SLF4J_REGEX.matches(line) -> false + LOMBOK_IMPORTS_REGEX.matches(line) -> false + else -> true } - return true } private fun deleteClone(target: File) { println(" --- Deleting temp folder ${target.absolutePath}") - project.execShell("rm", "-r", target.absolutePath) + execOperations.execShell("rm", "-r", target.absolutePath) println(" --- Deleted") } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/GenerateJsonSchemaTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/GenerateJsonSchemaTask.kt index fd8d1cfe1c..ff3f6f93dd 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/GenerateJsonSchemaTask.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/GenerateJsonSchemaTask.kt @@ -6,13 +6,18 @@ package com.datadog.gradle.plugin.jsonschema -import java.io.File +import com.datadog.gradle.plugin.jsonschema.generator.FileGenerator import org.gradle.api.DefaultTask import org.gradle.api.Task +import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction +import java.io.File +import java.nio.file.Paths // TODO test all from https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/master/tests/draft2019-09 @@ -21,73 +26,59 @@ import org.gradle.api.tasks.TaskAction * * It will read source JsonSchema files and generate the relevant Kotlin data classes. */ +@CacheableTask open class GenerateJsonSchemaTask : DefaultTask() { - private lateinit var extension: JsonSchemaExtension - init { group = "datadog" - description = "Review the Android benchmark results and ensure they fit the provided rules" + description = "Read source JSON schema files and generate the relevant Kotlin data classes" } - // region Task + // region Input/Output /** - * The main [TaskAction]. + * The [InputFiles] (E.g.: all the json files in `resources/json`). Note, that it will get all + * files recursively in order to keep Gradle build cache state in a proper way, but only + * top-level files are used for the model generation (they can be just a reference to the + * deeper files, so they may receive no changes making Gradle build cache to have + * a wrong caching decision when deeper files actually change). */ - @TaskAction - fun performTask() { - val inputDir = getInputDir() - val outputDir = getOutputDir() - val files = getInputFiles() - .filter { it.name !in extension.ignoredFiles } - - println("Found ${files.size} in input dir: $inputDir") - - val reader = JsonSchemaReader(extension.nameMapping) - val generator = PokoGenerator(outputDir, extension.targetPackageName) - files.forEach { - val type = reader.readSchema(it) - generator.generate(type) - } - } - - private fun getInputDir(): File { - return File("${project.projectDir.path}${File.separator}${extension.inputDirPath}") + @PathSensitive(PathSensitivity.RELATIVE) + @InputFiles + fun getInputFiles(): List { + return getInputDir() + .walkBottomUp() + .filter { + it.isFile && it.extension == "json" + } + .toList() } /** - * The [InputFiles] (E.g.: all the json files in `resources/json`). + * The directory from which to read the files json schema files. */ - @InputFiles - fun getInputFiles(): List { - return getInputDir().listFiles().orEmpty().toList() - .filter { it.extension == "json" } - } + @Input + var inputDirPath: String = "" /** - * The [Input] package name of generated classes (E.g.: `com.example.model`). + * The package name where to generate the models based on the schema files. + * (E.g.: `com.example.model`). */ @Input - fun getInputPackageName(): String { - return extension.inputDirPath - } + var targetPackageName: String = "" /** - * The [Input] a list of input file name to ignore + * The list of schema files to be ignored. */ @Input - fun getInputIgnoredFiles(): Array { - return extension.ignoredFiles - } + var ignoredFiles: Array = emptyArray() /** - * The [Input] a list of map from file name to type name + * The mapping of the schema file to the generated model name. Mostly used for merged + * schemas. */ @Input - fun getInputNameMapping(): Map { - return extension.nameMapping - } + var inputNameMapping: Map = emptyMap() /** * The [OutputDirectory] (`src/main/kotlin/{out_package}`). @@ -97,18 +88,47 @@ open class GenerateJsonSchemaTask : DefaultTask() { val topDir = getOutputDir() val outputPackageDir = File( topDir.absolutePath + File.separator + - extension.targetPackageName.replace('.', File.separatorChar) + targetPackageName.replace('.', File.separatorChar) ) return outputPackageDir } - internal fun setParams(extension: JsonSchemaExtension) { - this.extension = extension + // endregion + + // region Task action + + /** + * The main [TaskAction]. + */ + @TaskAction + fun performTask() { + val inputDir = getInputDir() + val outputDir = getOutputDir() + val files = getInputFiles() + .filter { + it.name !in ignoredFiles && it.parentFile == inputDir + } + + logger.info("Found ${files.size} files in input dir: $inputDir") + + val reader = JsonSchemaReader(inputNameMapping, logger) + val generator = FileGenerator(outputDir, targetPackageName, logger) + files.forEach { + val type = reader.readSchema(it) + generator.generate(type) + } + } + + private fun getInputDir(): File { + return File("${project.projectDir.path}${File.separator}$inputDirPath") } private fun getOutputDir(): File { - val srcDir = File(project.projectDir, "src") - val mainDir = File(srcDir, "main") + val json2kotlinDir = project.layout.buildDirectory + .dir(Paths.get("generated", "json2kotlin").toString()) + .get() + .asFile + val mainDir = File(json2kotlinDir, "main") val file = File(mainDir, "kotlin") if (!file.exists()) file.mkdirs() return file diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonDefinition.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonDefinition.kt index b10f8eb3b4..79df5f48c8 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonDefinition.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonDefinition.kt @@ -12,7 +12,7 @@ data class JsonDefinition( @SerializedName("title") val title: String?, @SerializedName("description") val description: String?, @SerializedName("type") val type: JsonType?, - @SerializedName("enum") val enum: List?, + @SerializedName("enum") val enum: List?, @SerializedName("const") val constant: Any?, @SerializedName("\$ref") val ref: String?, @SerializedName("\$id") val id: String?, @@ -20,6 +20,56 @@ data class JsonDefinition( @SerializedName("uniqueItems") val uniqueItems: Boolean?, @SerializedName("items") val items: JsonDefinition?, @SerializedName("allOf") val allOf: List?, + @SerializedName("oneOf") val oneOf: List?, + @SerializedName("anyOf") val anyOf: List?, @SerializedName("properties") val properties: Map?, - @SerializedName("definitions") val definitions: Map? -) + @SerializedName("definitions") val definitions: Map?, + @SerializedName("readOnly") val readOnly: Boolean?, + @SerializedName("additionalProperties") val additionalProperties: Any?, + @SerializedName("default") val default: Any? +) { + + companion object { + val EMPTY = JsonDefinition( + title = null, + description = null, + type = null, + enum = null, + constant = null, + ref = null, + id = null, + required = null, + uniqueItems = null, + items = null, + allOf = null, + oneOf = null, + anyOf = null, + properties = null, + definitions = null, + readOnly = null, + additionalProperties = null, + default = null + ) + + val ANY = JsonDefinition( + title = null, + description = null, + type = JsonType.OBJECT, + enum = null, + constant = null, + ref = null, + id = null, + required = null, + uniqueItems = null, + items = null, + allOf = null, + oneOf = null, + anyOf = null, + properties = null, + definitions = null, + readOnly = null, + additionalProperties = null, + default = null + ) + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonDefinitionReference.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonDefinitionReference.kt new file mode 100644 index 0000000000..3d6d3c5104 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonDefinitionReference.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema + +import java.io.File + +data class JsonDefinitionReference( + val typeName: String, + val definition: JsonDefinition, + val fromFile: File +) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonPrimitiveType.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonPrimitiveType.kt new file mode 100644 index 0000000000..af40e1bd9f --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonPrimitiveType.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema + +enum class JsonPrimitiveType { + STRING, BOOLEAN, INTEGER, DOUBLE, NUMBER +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaExtension.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaExtension.kt deleted file mode 100644 index 192f9e3a76..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaExtension.kt +++ /dev/null @@ -1,18 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.jsonschema - -/** - * The main Gradle extension. - * This allows you to define rules for verifying Jetpack Benchmark results. - */ -open class JsonSchemaExtension { - var targetPackageName: String = "" - var inputDirPath: String = "resources" - var ignoredFiles: Array = emptyArray() - var nameMapping: Map = emptyMap() -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaPlugin.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaPlugin.kt deleted file mode 100644 index 74378a53db..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaPlugin.kt +++ /dev/null @@ -1,39 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.jsonschema - -import org.gradle.api.Plugin -import org.gradle.api.Project - -/** - * The main Gradle [Plugin]. - */ -class JsonSchemaPlugin : Plugin { - - // region Plugin - - /** - * {@inheritDoc}. - */ - override fun apply(target: Project) { - val extension = target.extensions - .create(EXTENSION_NAME, JsonSchemaExtension::class.java) - - val task = target.tasks - .create(TASK_REVIEW_NAME, GenerateJsonSchemaTask::class.java) - task.setParams(extension = extension) - - target.tasks.named("preBuild") { dependsOn(task) } - } - - // endregion - - companion object { - internal const val EXTENSION_NAME = "jsonSchema2Poko" - internal const val TASK_REVIEW_NAME = "generateJsonSchema2Poko" - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReader.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReader.kt index 4b26f51ba3..22999dc191 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReader.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReader.kt @@ -6,33 +6,34 @@ package com.datadog.gradle.plugin.jsonschema -import android.databinding.tool.ext.toCamelCase +import com.datadog.gradle.utils.toCamelCase import com.google.gson.Gson +import org.gradle.api.logging.Logger import java.io.File class JsonSchemaReader( - internal val nameMapping: Map + private val nameMapping: Map, + private val logger: Logger ) { private val gson = Gson() private lateinit var currentFile: File private val loadedSchemas: MutableList = mutableListOf() + private val knownSchemas: MutableMap = mutableMapOf() // region JsonSchemaReader fun readSchema(schemaFile: File): TypeDefinition { - println("Reading schema ${schemaFile.name}") + logger.info("Reading schema ${schemaFile.name}") val schema = loadSchema(schemaFile) - require(schema.type == JsonType.OBJECT) { - "Top level schema with type ${schema.type} is not supported." - } currentFile = schemaFile val customName = nameMapping[schemaFile.name] val fileName = schemaFile.nameWithoutExtension val typeName = (customName ?: schema.title ?: fileName).toCamelCase() - return transform(schema, typeName) + val rawType = transform(schema, typeName, schemaFile) + return sanitize(rawType) } // endregion @@ -45,18 +46,25 @@ class JsonSchemaReader( * - `#/definitions/foo` * - `#bar` * - * @param ref the reference used to resolve the target definiton + * @param fromFile the file where the reference is being used + * @param ref the reference used to resolve the target definition * @return the found Definition reference or null */ + @Suppress("ReturnCount") private fun findDefinitionReference( - ref: String - ): Pair? { + ref: String, + fromFile: File + ): JsonDefinitionReference? { val file = REF_FILE_REGEX.matchEntire(ref) if (file != null) { - return loadDefinitionFromFileRef(file.groupValues[2], file.groupValues[5]) + val path = file.groupValues[2] + val localRef = file.groupValues[5] + return loadDefinitionFromFileRef(path, localRef, fromFile) } - val name = REF_DEFINITION_REGEX.matchEntire(ref)?.groupValues?.get(1) + val fieldGroups = REF_NAME_REGEX.matchEntire(ref)?.groupValues + val type = fieldGroups?.get(1) + val name = fieldGroups?.get(2) val id = REF_ID_REGEX.matchEntire(ref)?.groupValues?.get(0) if (name == null && id == null) return null @@ -66,31 +74,44 @@ class JsonSchemaReader( { _: String, def: JsonDefinition -> def.id == id } } - val match = loadedSchemas.mapNotNull { schema -> - schema.definitions?.entries?.firstOrNull { matcher(it.key, it.value) } - }.firstOrNull() ?: return null + val match = knownSchemas[fromFile] + ?.let { + // only explicit properties lookup supported + if (type == REF_TYPE_PROPERTIES) it.properties else it.definitions + } + ?.entries + ?.firstOrNull { matcher(it.key, it.value) } ?: return null - return match.key.toCamelCase() to match.value + return JsonDefinitionReference( + match.key.toCamelCase(), + match.value, + fromFile + ) } /** * Loads a Definition from a Json Schema file * @param path the path to the file to load (relatively to the root file) - * @param ref a nested reference to a definiton, or blank to use the root type. + * @param ref a nested reference to a definition, or blank to use the root type. * @return the loaded definition or `null` if it wasn't found */ private fun loadDefinitionFromFileRef( path: String, - ref: String - ): Pair? { - val file = File(currentFile.parentFile.absolutePath + File.separator + path) + ref: String, + fromFile: File + ): JsonDefinitionReference? { + val file = File(fromFile.parentFile.absolutePath + File.separator + path) val schema = loadSchema(file) return if (ref.isBlank()) { val className = (schema.title ?: file.nameWithoutExtension).toCamelCase() - return className to schema + return JsonDefinitionReference( + className, + schema, + file + ) } else { - findDefinitionReference(ref) + findDefinitionReference(ref, file) } } @@ -100,11 +121,18 @@ class JsonSchemaReader( * @return the loaded [JsonDefinition] */ private fun loadSchema(file: File): JsonDefinition { + val knownSchema = knownSchemas[file] + if (knownSchema != null) { + return knownSchema + } + val schema = gson.fromJson( file.inputStream().reader(Charsets.UTF_8), JsonDefinition::class.java ) loadedSchemas.add(schema) + knownSchemas[file] = schema + return schema } @@ -126,27 +154,74 @@ class JsonSchemaReader( // region Internal + @Suppress("FunctionMaxLength") + private fun extractAdditionalProperties( + definition: JsonDefinition, + fromFile: File + ): TypeProperty? { + return when (val additional = definition.additionalProperties) { + null -> null // TODO additionalProperties is true by default ! + is Map<*, *> -> { + val type = additional["type"]?.toString() + val readOnly = additional["readOnly"] as? Boolean + if (type == null) { + error("additionalProperties object is missing a `type`") + } else { + val jsonType = JsonType.values().firstOrNull { it.name.equals(type, true) } + val typeDef = transform(JsonDefinition.ANY.copy(type = jsonType), "?", fromFile) + TypeProperty( + name = "", + type = typeDef, + optional = true, + readOnly = readOnly ?: false, + defaultValue = null + ) + } + } + + is Boolean -> { + if (additional) { + val typeDef = transform(JsonDefinition.ANY.copy(type = JsonType.OBJECT), "?", fromFile) + TypeProperty( + name = "", + type = typeDef, + optional = true, + readOnly = false, + defaultValue = null + ) + } else { + null + } + } + + else -> { + error("additionalProperties uses an unknown format") + } + } + } + private fun transform( definition: JsonDefinition?, - typeName: String + typeName: String, + fromFile: File ): TypeDefinition { - if (definition == null) return TypeDefinition.Null() - val type = definition.type - return when (type) { + return when (definition.type) { JsonType.NULL -> TypeDefinition.Null(definition.description.orEmpty()) - JsonType.BOOLEAN, - JsonType.NUMBER, - JsonType.INTEGER, - JsonType.STRING -> transformPrimitive(definition, typeName) - JsonType.ARRAY -> transformArray(definition, typeName) - JsonType.OBJECT, null -> transformType(definition, typeName) + JsonType.BOOLEAN -> transformPrimitive(definition, JsonPrimitiveType.BOOLEAN, typeName) + JsonType.NUMBER -> transformPrimitive(definition, JsonPrimitiveType.NUMBER, typeName) + JsonType.INTEGER -> transformPrimitive(definition, JsonPrimitiveType.INTEGER, typeName) + JsonType.STRING -> transformPrimitive(definition, JsonPrimitiveType.STRING, typeName) + JsonType.ARRAY -> transformArray(definition, typeName, fromFile) + JsonType.OBJECT, + null -> transformType(definition, typeName, fromFile) } } private fun transformPrimitive( definition: JsonDefinition, + primitiveType: JsonPrimitiveType, typeName: String ): TypeDefinition { return if (!definition.enum.isNullOrEmpty()) { @@ -155,7 +230,7 @@ class JsonSchemaReader( transformConstant(definition.type, definition.constant, definition.description) } else { TypeDefinition.Primitive( - type = definition.type ?: JsonType.NULL, + type = primitiveType, description = definition.description.orEmpty() ) } @@ -164,7 +239,7 @@ class JsonSchemaReader( private fun transformEnum( typeName: String, type: JsonType?, - values: List, + values: List, description: String? ): TypeDefinition.Enum { return TypeDefinition.Enum( @@ -189,12 +264,13 @@ class JsonSchemaReader( private fun transformArray( definition: JsonDefinition, - typeName: String + typeName: String, + fromFile: File ): TypeDefinition { val singularName = typeName.singular() val items = definition.items return TypeDefinition.Array( - items = transform(items, singularName), + items = transform(items, singularName, fromFile), uniqueItems = definition.uniqueItems ?: false, description = definition.description.orEmpty() ) @@ -202,38 +278,90 @@ class JsonSchemaReader( private fun transformType( definition: JsonDefinition, - typeName: String + typeName: String, + fromFile: File ): TypeDefinition { return if (!definition.enum.isNullOrEmpty()) { transformEnum(typeName, definition.type, definition.enum, definition.description) } else if (definition.constant != null) { transformConstant(definition.type, definition.constant, definition.description) - } else if (!definition.properties.isNullOrEmpty()) { - generateDataClass(typeName, definition) + } else if (!definition.properties.isNullOrEmpty() || + definition.additionalProperties != null + ) { + generateDataClass(typeName, definition, fromFile) } else if (!definition.allOf.isNullOrEmpty()) { - generateTypeAllOf(typeName, definition.allOf) + generateTypeAllOf(typeName, definition.allOf, fromFile) + } else if (!definition.oneOf.isNullOrEmpty()) { + generateTypeOneOf(typeName, definition.oneOf, definition.description, fromFile) + } else if (!definition.anyOf.isNullOrEmpty()) { + // for now let's consider anyOf to be equivalent to oneOf + generateTypeOneOf(typeName, definition.anyOf, definition.description, fromFile) } else if (!definition.ref.isNullOrBlank()) { - val refDefinition = findDefinitionReference(definition.ref) + val refDefinition = findDefinitionReference(definition.ref, fromFile) if (refDefinition != null) { - transform(refDefinition.second, refDefinition.first) + transform(refDefinition.definition, refDefinition.typeName, refDefinition.fromFile) } else { - throw IllegalStateException( + error( "Definition reference not found: ${definition.ref}." ) } + } else if (definition.type == JsonType.OBJECT) { + generateDataClass(typeName, definition, fromFile) } else { throw UnsupportedOperationException("Unsupported schema definition\n$definition") } } + private fun generateTypeOneOf( + typeName: String, + oneOf: List, + description: String?, + fromFile: File + ): TypeDefinition { + val options = oneOf.mapIndexed { i, type -> + transform(type, type.title ?: "${typeName}_$i", fromFile) + } + + val asArray = options.filterIsInstance().firstOrNull() + + return if (options.isEmpty()) { + TypeDefinition.Null(description.orEmpty()) + } else if (options.size == 1) { + options.first() + } else if (asArray != null) { + // we're (probably) in a case with `oneOf(, Array)` + // because we can't make a type matching both, we simplify it to be always an array + logger.warn("Simplifying a 'oneOf' constraint to $asArray") + asArray + } else if (options.all { it is TypeDefinition.Class || it is TypeDefinition.OneOfClass }) { + TypeDefinition.OneOfClass( + typeName, + options.flatMap { + when (it) { + is TypeDefinition.OneOfClass -> it.options + is TypeDefinition.Class -> listOf(it) + else -> emptyList() + } + }, + description.orEmpty() + ) + } else { + throw UnsupportedOperationException( + "Unable to implement `oneOf` constraint with types:\n " + + options.joinToString("\n ") + ) + } + } + private fun generateTypeAllOf( typeName: String, - allOf: List + allOf: List, + fromFile: File ): TypeDefinition { var mergedType: TypeDefinition = TypeDefinition.Class(typeName, emptyList()) allOf.forEach { - val type = transform(it, typeName) + val type = transform(it, typeName, fromFile) mergedType = mergedType.mergedWith(type) } return mergedType @@ -241,31 +369,63 @@ class JsonSchemaReader( private fun generateDataClass( typeName: String, - definition: JsonDefinition + definition: JsonDefinition, + fromFile: File ): TypeDefinition { - val properties = mutableListOf() definition.properties?.forEach { (name, property) -> val required = (definition.required != null) && (name in definition.required) + val readOnly = (property.readOnly == null) || (property.readOnly) val propertyType = transform( property, - name.toCamelCase() + name.toCamelCase(), + fromFile + ) + properties.add( + TypeProperty( + name, + propertyType, + !required, + readOnly, + property.default + ) ) - properties.add(TypeProperty(name, propertyType, !required)) } + val additional = extractAdditionalProperties(definition, fromFile) return TypeDefinition.Class( name = typeName, description = definition.description.orEmpty(), - properties = properties + properties = properties, + additionalProperties = additional ) } + private fun sanitize(type: TypeDefinition): TypeDefinition { + if (type is TypeDefinition.Class) { + val names = type.getChildrenTypeNames().distinct().sortedBy { it.first } + val duplicates = mutableSetOf() + + names.forEachIndexed { i, n -> + if (i > 0) { + if (n.first == names[i - 1].first) { + duplicates.add(n.first) + } + } + } + + return type.renameRecursive(duplicates, "") + } else { + return type + } + } // endregion companion object { + private const val REF_TYPE_PROPERTIES = "properties" + private const val REF_TYPE_DEFINITIONS = "definitions" - private val REF_DEFINITION_REGEX = Regex("#/definitions/([\\w]+)") + private val REF_NAME_REGEX = Regex("#/($REF_TYPE_DEFINITIONS|$REF_TYPE_PROPERTIES)/([\\w]+)") private val REF_ID_REGEX = Regex("#[\\w]+") private val REF_FILE_REGEX = Regex("(file:)?(([^/]+/)*([^/]+)\\.json)(#(.*))?") } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonType.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonType.kt index c295044845..a608583f42 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonType.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/JsonType.kt @@ -28,5 +28,17 @@ enum class JsonType { STRING, @SerializedName("integer") - INTEGER + INTEGER; + + fun asJsonPrimitiveType(): JsonPrimitiveType? { + return when (this) { + BOOLEAN -> JsonPrimitiveType.BOOLEAN + NUMBER -> JsonPrimitiveType.NUMBER + STRING -> JsonPrimitiveType.STRING + INTEGER -> JsonPrimitiveType.INTEGER + ARRAY, + OBJECT, + NULL -> null + } + } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/PokoExtensions.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/PokoExtensions.kt new file mode 100644 index 0000000000..15c297a557 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/PokoExtensions.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema + +import com.datadog.gradle.utils.joinToCamelCaseAsVar +import com.squareup.kotlinpoet.BOOLEAN +import com.squareup.kotlinpoet.DOUBLE +import com.squareup.kotlinpoet.LONG +import com.squareup.kotlinpoet.NOTHING +import com.squareup.kotlinpoet.NUMBER +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.TypeName + +internal val NOTHING_NULLABLE = NOTHING.copy(nullable = true) + +@Suppress("ReturnCount") +internal fun String.variableName(): String { + val split = this.split("_").filter { it.isNotBlank() } + if (split.isEmpty()) return "" + if (split.size == 1) return split[0] + return split.joinToCamelCaseAsVar() +} + +internal fun JsonType?.asKotlinTypeName(): TypeName { + return when (this) { + null, + JsonType.NULL -> NOTHING_NULLABLE + JsonType.BOOLEAN -> BOOLEAN + JsonType.NUMBER -> NUMBER + JsonType.STRING -> STRING + JsonType.INTEGER -> LONG + JsonType.OBJECT, + JsonType.ARRAY -> throw IllegalArgumentException( + "Cannot convert $this to a KotlinTypeName" + ) + } +} + +internal fun JsonPrimitiveType?.asKotlinTypeName(): TypeName { + return when (this) { + JsonPrimitiveType.BOOLEAN -> BOOLEAN + JsonPrimitiveType.DOUBLE -> DOUBLE + JsonPrimitiveType.STRING -> STRING + JsonPrimitiveType.INTEGER -> LONG + JsonPrimitiveType.NUMBER -> NUMBER + null -> NOTHING_NULLABLE + } +} + +internal fun TypeName.asNullable() = copy(nullable = true) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/PokoGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/PokoGenerator.kt deleted file mode 100644 index 5590cc9315..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/PokoGenerator.kt +++ /dev/null @@ -1,528 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.jsonschema - -import android.databinding.tool.ext.joinToCamelCaseAsVar -import com.squareup.kotlinpoet.ANY -import com.squareup.kotlinpoet.BOOLEAN -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.CodeBlock -import com.squareup.kotlinpoet.DOUBLE -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.LIST -import com.squareup.kotlinpoet.LONG -import com.squareup.kotlinpoet.NOTHING -import com.squareup.kotlinpoet.ParameterSpec -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.SET -import com.squareup.kotlinpoet.STRING -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.TypeSpec -import java.io.File -import java.util.Locale - -class PokoGenerator( - internal val outputDir: File, - internal val packageName: String -) { - - private lateinit var rootTypeName: String - private val knownTypes: MutableList = mutableListOf() - - private val nestedClasses: MutableSet = mutableSetOf() - private val nestedEnums: MutableSet = mutableSetOf() - - // region PokoGenerator - - /** - * Generate a POKO file based on the input schema file - */ - fun generate(typeDefinition: TypeDefinition) { - println("Generating class for type $typeDefinition with package name $packageName") - knownTypes.clear() - nestedClasses.clear() - nestedEnums.clear() - generateFile(typeDefinition) - } - - // endregion - - // region Code Generation - - /** - * Generate a POKO file based on the root schema definition - */ - private fun generateFile(definition: TypeDefinition) { - check(definition is TypeDefinition.Class) - - rootTypeName = definition.name - val fileBuilder = FileSpec.builder(packageName, definition.name) - val typeBuilder = generateClass(definition) - .addModifiers(KModifier.INTERNAL) - - while (nestedClasses.isNotEmpty()) { - val definitions = nestedClasses.toList() - definitions.forEach { - typeBuilder.addType(generateClass(it).build()) - } - nestedClasses.removeAll(definitions) - } - - nestedEnums.forEach { - typeBuilder.addType(generateEnumClass(it)) - } - - fileBuilder - .addType(typeBuilder.build()) - .indent(" ") - .build() - .writeTo(outputDir) - } - - /** - * Generates the `class` [TypeSpec.Builder] for the given definition. - * @param definition the definition of the type - */ - private fun generateClass( - definition: TypeDefinition.Class - ): TypeSpec.Builder { - val constructorBuilder = FunSpec.constructorBuilder() - val typeBuilder = TypeSpec.classBuilder(definition.name) - val docBuilder = CodeBlock.builder() - - appendTypeDefinition( - definition, - typeBuilder, - constructorBuilder, - docBuilder - ) - - typeBuilder.primaryConstructor(constructorBuilder.build()) - .addKdoc(docBuilder.build()) - - return typeBuilder - } - - /** - * Generates the `enum class` [TypeSpec.Builder] for the given definition. - * @param definition the enum class definition - */ - private fun generateEnumClass( - definition: TypeDefinition.Enum - ): TypeSpec { - val enumBuilder = TypeSpec.enumBuilder(definition.name) - val docBuilder = CodeBlock.builder() - - if (definition.description.isNotBlank()) { - docBuilder.add(definition.description) - docBuilder.add("\n") - } - - definition.values.forEach { value -> - enumBuilder.addEnumConstant( - value.toUpperCase(Locale.US), - TypeSpec.anonymousClassBuilder() - .build() - ) - } - - enumBuilder.addFunction(generateEnumSerializer(definition)) - - return enumBuilder - .addKdoc(docBuilder.build()) - .build() - } - - /** - * Appends a property to a [TypeSpec.Builder]. - * @param property the property definition - * @param typeBuilder the `data class` [TypeSpec] builder. - * @param constructorBuilder the `data class` constructor builder. - * @param docBuilder the `data class` KDoc builder. - */ - private fun appendProperty( - property: TypeProperty, - typeBuilder: TypeSpec.Builder, - constructorBuilder: FunSpec.Builder, - docBuilder: CodeBlock.Builder - ) { - val varName = property.name.variableName() - val nullable = property.optional || property.type is TypeDefinition.Null - val type = property.type.asKotlinTypeName() - .copy(nullable = nullable) - - val constructorParamBuilder = ParameterSpec.builder(varName, type) - if (nullable) { - constructorParamBuilder.defaultValue("null") - } - constructorBuilder.addParameter(constructorParamBuilder.build()) - - typeBuilder.addProperty( - PropertySpec.builder(varName, type) - .initializer(varName) - .build() - ) - - if (property.type.description.isNotBlank()) { - docBuilder.add("@param $varName ${property.type.description}\n") - } - } - - /** - * Appends a property to a [TypeSpec.Builder], with a constant default value. - * @param name the property json name - * @param definition the property definition - * @param typeBuilder the `data class` [TypeSpec] builder. - */ - private fun appendConstant( - name: String, - definition: TypeDefinition.Constant, - typeBuilder: TypeSpec.Builder - ) { - val varName = name.variableName() - val constantValue = definition.value - val propertyBuilder = if (constantValue is String) { - PropertySpec.builder(varName, STRING) - .initializer("\"$constantValue\"") - } else if (constantValue is Double && definition.type == JsonType.INTEGER) { - PropertySpec.builder(varName, LONG) - .initializer("${constantValue.toLong()}L") - } else if (constantValue is Double) { - PropertySpec.builder(varName, DOUBLE) - .initializer("$constantValue") - } else if (constantValue is Boolean) { - PropertySpec.builder(varName, BOOLEAN) - .initializer("$constantValue") - } else { - throw IllegalStateException("Unable to generate constant type $definition") - } - - if (definition.description.isNotBlank()) { - propertyBuilder.addKdoc(definition.description) - } - - typeBuilder.addProperty(propertyBuilder.build()) - } - - /** - * Appends all properties to a [TypeSpec.Builder] from the given definition. - * @param definition the definition to use. - * @param typeBuilder the `data class` [TypeSpec] builder. - * @param constructorBuilder the `data class` constructor builder. - * @param docBuilder the `data class` KDoc builder. - */ - private fun appendTypeDefinition( - definition: TypeDefinition.Class, - typeBuilder: TypeSpec.Builder, - constructorBuilder: FunSpec.Builder, - docBuilder: CodeBlock.Builder - ) { - if (definition.description.isNotBlank()) { - docBuilder.add(definition.description) - docBuilder.add("\n") - } - - var nonConstants = 0 - - definition.properties.forEach { p -> - if (p.type is TypeDefinition.Constant) { - appendConstant(p.name, p.type, typeBuilder) - } else { - nonConstants++ - appendProperty( - p, - typeBuilder, - constructorBuilder, - docBuilder - ) - } - } - - if (nonConstants > 0) { - typeBuilder.addModifiers(KModifier.DATA) - } - - typeBuilder.addFunction(generateClassSerializer(definition)) - } - - // endregion - - // region Serialization - - /** - * Generates a function serializing the type to Json - * @param definition the class definition - */ - private fun generateClassSerializer(definition: TypeDefinition.Class): FunSpec { - val funBuilder = FunSpec.builder(TO_JSON) - .returns(JSON_ELEMENT) - - funBuilder.addStatement("val json = %T()", JSON_OBJECT) - - definition.properties.forEach { p -> - appendPropertySerialization(p, funBuilder) - } - - funBuilder.addStatement("return json") - - return funBuilder.build() - } - - /** - * Generates a function serializing the type to Json - * @param definition the enum class definition - */ - private fun generateEnumSerializer(definition: TypeDefinition.Enum): FunSpec { - val funBuilder = FunSpec.builder(TO_JSON) - .returns(JSON_ELEMENT) - - funBuilder.beginControlFlow("return when (this)") - definition.values.forEach { value -> - funBuilder.addStatement( - "%L -> %T(%S)", - value.toUpperCase(Locale.US), - JSON_PRIMITIVE, - value - ) - } - funBuilder.endControlFlow() - - return funBuilder.build() - } - - /** - * Appends a property serialization to a [FunSpec.Builder]. - * @param property the property definition - * @param funBuilder the `toJson()` [FunSpec] builder. - */ - private fun appendPropertySerialization( - property: TypeProperty, - funBuilder: FunSpec.Builder - ) { - - val varName = property.name.variableName() - when (property.type) { - is TypeDefinition.Constant -> appendConstantSerialization( - property.name, - property.type, - funBuilder - ) - is TypeDefinition.Primitive -> appendPrimitiveSerialization( - property, - property.type, - varName, - funBuilder - ) - is TypeDefinition.Null -> if (!property.optional) { - funBuilder.addStatement("json.add(%S, null)", property.name) - } - is TypeDefinition.Array -> appendArraySerialization( - property, - property.type, - varName, - funBuilder - ) - is TypeDefinition.Class, - is TypeDefinition.Enum -> appendObjectSerialization(property, varName, funBuilder) - } - } - - private fun appendObjectSerialization( - property: TypeProperty, - varName: String, - funBuilder: FunSpec.Builder - ) { - if (property.optional) { - funBuilder.addStatement( - "if (%L != null) json.add(%S, %L.%L())", - varName, - property.name, - varName, - TO_JSON - ) - } else { - funBuilder.addStatement( - "json.add(%S, %L.%L())", - property.name, - varName, TO_JSON - ) - } - } - - private fun appendArraySerialization( - property: TypeProperty, - type: TypeDefinition.Array, - varName: String, - funBuilder: FunSpec.Builder - ) { - if (property.optional) { - funBuilder.beginControlFlow("if (%L != null)", varName) - } - - funBuilder.addStatement("val %LArray = %T(%L.size)", varName, JSON_ARRAY, varName) - when (type.items) { - is TypeDefinition.Primitive -> funBuilder.addStatement( - "%L.forEach { %LArray.add(it) }", - varName, - varName - ) - is TypeDefinition.Class, - is TypeDefinition.Enum -> funBuilder.addStatement( - "%L.forEach { %LArray.add(it.%L()) }", - varName, - varName, - TO_JSON - ) - } - - funBuilder.addStatement("json.add(%S, %LArray)", property.name, varName) - - if (property.optional) { - funBuilder.endControlFlow() - } - } - - /** - * Appends a primitive property serialization to a [FunSpec.Builder]. - * @param property the property definition - * @param type the primitive type - * @param funBuilder the `toJson()` [FunSpec] builder. - */ - @Suppress("NON_EXHAUSTIVE_WHEN") - private fun appendPrimitiveSerialization( - property: TypeProperty, - type: TypeDefinition.Primitive, - varName: String, - funBuilder: FunSpec.Builder - ) { - when (type.type) { - JsonType.BOOLEAN, - JsonType.NUMBER, - JsonType.STRING, - JsonType.INTEGER -> - if (property.optional) { - funBuilder.addStatement( - "if (%L != null) json.addProperty(%S, %L)", - varName, - property.name, - varName - ) - } else { - funBuilder.addStatement( - "json.addProperty(%S, %L)", - property.name, - varName - ) - } - } - } - - /** - * Appends a property serialization to a [FunSpec.Builder], with a constant default value. - * @param name the property json name - * @param definition the property definition - * @param funBuilder the `toJson()` [FunSpec] builder. - */ - private fun appendConstantSerialization( - name: String, - definition: TypeDefinition.Constant, - funBuilder: FunSpec.Builder - ) { - val constantValue = definition.value - if (constantValue is String || constantValue is Number) { - funBuilder.addStatement("json.addProperty(%S, %L)", name, name.variableName()) - } else { - throw IllegalStateException("Unable to generate serialization for constant type $definition") - } - } - - // endregion - - // region Extensions - - private fun String.variableName(): String { - val split = this.split("_").filter { it.isNotBlank() } - if (split.isEmpty()) return "" - if (split.size == 1) return split[0] - return split.joinToCamelCaseAsVar() - } - - private fun String.uniqueClassName(): String { - var uniqueName = this - var tries = 0 - while (uniqueName in knownTypes) { - tries++ - uniqueName = "${this}$tries" - } - knownTypes.add(uniqueName) - return uniqueName - } - - private fun TypeDefinition.Enum.withUniqueTypeName(): TypeDefinition.Enum { - val matchingEnum = nestedEnums.firstOrNull { it.values == values } - return matchingEnum ?: copy(name = name.uniqueClassName()) - } - - private fun TypeDefinition.Class.withUniqueTypeName(): TypeDefinition.Class { - val matchingClass = nestedClasses.firstOrNull { it.properties == properties } - return matchingClass ?: copy(name = name.uniqueClassName()) - } - - private fun TypeDefinition.asKotlinTypeName(): TypeName { - return when (this) { - is TypeDefinition.Null -> NOTHING - is TypeDefinition.Primitive -> type.asKotlinTypeName() - is TypeDefinition.Constant -> type.asKotlinTypeName() - is TypeDefinition.Class -> { - val def = withUniqueTypeName() - nestedClasses.add(def) - ClassName(packageName, rootTypeName, def.name) - } - is TypeDefinition.Array -> { - if (uniqueItems) { - SET.parameterizedBy(items.asKotlinTypeName()) - } else { - LIST.parameterizedBy(items.asKotlinTypeName()) - } - } - is TypeDefinition.Enum -> { - val def = withUniqueTypeName() - nestedEnums.add(def) - ClassName(packageName, rootTypeName, def.name) - } - } - } - - private fun JsonType?.asKotlinTypeName(): TypeName { - return when (this) { - JsonType.NULL -> NOTHING_NULLABLE - JsonType.BOOLEAN -> BOOLEAN - JsonType.NUMBER -> DOUBLE - JsonType.STRING -> STRING - JsonType.INTEGER -> LONG - else -> TODO() - } - } - - // endregion - - companion object { - - private val NOTHING_NULLABLE = NOTHING.copy(nullable = true) - private val ANY_NULLABLE = ANY.copy(nullable = true) - - private val TO_JSON = "toJson" - - private val JSON_ELEMENT = ClassName.bestGuess("com.google.gson.JsonElement") - private val JSON_OBJECT = ClassName.bestGuess("com.google.gson.JsonObject") - private val JSON_ARRAY = ClassName.bestGuess("com.google.gson.JsonArray") - private val JSON_PRIMITIVE = ClassName.bestGuess("com.google.gson.JsonPrimitive") - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeDefinition.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeDefinition.kt index dbe047508d..0995b610a2 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeDefinition.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeDefinition.kt @@ -6,18 +6,27 @@ package com.datadog.gradle.plugin.jsonschema +import java.util.Locale +import kotlin.reflect.KClass + sealed class TypeDefinition { abstract val description: String abstract fun mergedWith(other: TypeDefinition): TypeDefinition + abstract fun matches(other: TypeDefinition): Boolean + data class Null( override val description: String = "" ) : TypeDefinition() { override fun mergedWith(other: TypeDefinition): TypeDefinition { return other } + + override fun matches(other: TypeDefinition): Boolean { + return other is Null + } } data class Constant( @@ -26,19 +35,51 @@ sealed class TypeDefinition { override val description: String = "" ) : TypeDefinition() { override fun mergedWith(other: TypeDefinition): TypeDefinition { - throw IllegalStateException("Can't merge Constant with type $other") + error("Can't merge Constant with type $other") + } + + override fun matches(other: TypeDefinition): Boolean { + return (other is Constant) && (other.type == type) && (other.value == value) + } + + fun asPrimitiveTypeFun(): String { + return when (type) { + JsonType.NULL -> "asNull" + JsonType.BOOLEAN -> "asBoolean" + JsonType.NUMBER -> "asNumber" + JsonType.STRING -> "asString" + JsonType.INTEGER -> "asLong" + JsonType.ARRAY -> "asArray" + JsonType.OBJECT -> "asObjext" + null -> TODO() + } } } data class Primitive( - val type: JsonType, + val type: JsonPrimitiveType, override val description: String = "" ) : TypeDefinition() { + override fun mergedWith(other: TypeDefinition): TypeDefinition { if (other is Primitive && type == other.type) { return Primitive(type, "$description\n${other.description}".trim()) } else { - throw IllegalStateException("Can't merge Primitive with type $other") + error("Can't merge Primitive with type $other") + } + } + + override fun matches(other: TypeDefinition): Boolean { + return (other is Primitive) && (other.type == type) + } + + fun asPrimitiveTypeFun(): String { + return when (type) { + JsonPrimitiveType.BOOLEAN -> "asBoolean" + JsonPrimitiveType.DOUBLE -> "asDouble" + JsonPrimitiveType.STRING -> "asString" + JsonPrimitiveType.INTEGER -> "asLong" + JsonPrimitiveType.NUMBER -> "asNumber" } } } @@ -49,15 +90,22 @@ sealed class TypeDefinition { override val description: String = "" ) : TypeDefinition() { override fun mergedWith(other: TypeDefinition): TypeDefinition { - TODO("Not yet implemented") + error("Can't merge Array with type $other") + } + + override fun matches(other: TypeDefinition): Boolean { + return (other is Array) && (other.items == items) && (other.uniqueItems == uniqueItems) } } data class Class( val name: String, val properties: List, - override val description: String = "" + override val description: String = "", + val additionalProperties: TypeProperty? = null, + val parentType: OneOfClass? = null ) : TypeDefinition() { + override fun mergedWith(other: TypeDefinition): TypeDefinition { check(other is Class) { "Cannot merge Class with ${other.javaClass}" } @@ -78,22 +126,136 @@ sealed class TypeDefinition { } } + val mergedAdditionalProperties = + if (this.additionalProperties == null) { + other.additionalProperties + } else if (other.additionalProperties == null) { + this.additionalProperties + } else { + additionalProperties.mergedWith(other.additionalProperties) + } + return Class( name, mergedFields, - "$description\n${other.description}".trim() + "$description\n${other.description}".trim(), + mergedAdditionalProperties ) } + + override fun matches(other: TypeDefinition): Boolean { + return (other is Class) && + (other.properties == properties) && + (other.additionalProperties == additionalProperties) + } + + fun isConstantClass(): Boolean { + // all the properties are of type Constant and the additionalProperties is null + this.properties.forEach { + if (it.type !is Constant) { + return false + } + } + return this.additionalProperties == null // TODO false + } + + fun getChildrenTypeNames(): List> { + val direct = properties.map { it.type } + .mapNotNull { + when (it) { + is Class -> it.name to it.toString() + is Enum -> it.name to it.toString() + else -> null + } + } + val indirect = properties.map { it.type } + .mapNotNull { (it as? Class)?.getChildrenTypeNames() } + .flatten() + + return direct + indirect + (name to toString()) + } + + fun renameRecursive(duplicates: Set, parentName: String): TypeDefinition { + val newName = if (name in duplicates) { + "$parentName$name" + } else { + name + } + + val newProperties = properties.map { + if (it.type is Class) { + it.copy(type = it.type.renameRecursive(duplicates, newName)) + } else if (it.type is Enum) { + it.copy(type = it.type.rename(duplicates, newName)) + } else { + it + } + } + + return copy(name = newName, properties = newProperties) + } } data class Enum( val name: String, val type: JsonType?, - val values: List, + val values: List, + override val description: String = "" + ) : TypeDefinition() { + + override fun mergedWith(other: TypeDefinition): TypeDefinition { + error("Can't merge Enum with type $other") + } + + override fun matches(other: TypeDefinition): Boolean { + return (other is Enum) && (other.type == type) && (other.values == values) + } + + fun jsonValueType(): KClass<*> { + return when (type) { + JsonType.NUMBER -> Number::class + JsonType.STRING, JsonType.OBJECT, null -> String::class + else -> error("Not yet implemented") + } + } + + internal fun rename(duplicates: Set, parentName: String): TypeDefinition { + return if (name in duplicates) { + copy(name = "$parentName$name") + } else { + this + } + } + + internal fun allowsNull(): Boolean = values.any { it == null } + + internal fun enumConstantName(constantName: String?): String { + return if (constantName == null) { + "${name.uppercase(Locale.US)}_NULL" + } else if (type == JsonType.NUMBER) { + "${name.uppercase(Locale.US)}_${constantName.sanitizedName()}" + } else { + constantName.sanitizedName() + } + } + + private fun String.sanitizedName(): String { + return uppercase(Locale.US).replace(Regex("[^A-Z0-9]+"), "_") + } + } + + data class OneOfClass( + val name: String, + val options: List, override val description: String = "" ) : TypeDefinition() { + override fun mergedWith(other: TypeDefinition): TypeDefinition { - TODO("Not yet implemented") + error("Can't merge Multiclass with type $other") + } + + override fun matches(other: TypeDefinition): Boolean { + return (other is OneOfClass) && (other.options == options) } } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeProperty.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeProperty.kt index 67b0b0ac52..b60ce7d668 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeProperty.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/TypeProperty.kt @@ -9,7 +9,9 @@ package com.datadog.gradle.plugin.jsonschema data class TypeProperty( val name: String, val type: TypeDefinition, - val optional: Boolean + val optional: Boolean, + val readOnly: Boolean = true, + val defaultValue: Any? = null ) { fun mergedWith(other: TypeProperty): TypeProperty { return if (this == other) { @@ -18,7 +20,7 @@ data class TypeProperty( TypeProperty( name, type.mergedWith(other.type), - optional || other.optional + optional && other.optional ) } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassGenerator.kt new file mode 100644 index 0000000000..14c6fb44f8 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassGenerator.kt @@ -0,0 +1,479 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.JsonPrimitiveType +import com.datadog.gradle.plugin.jsonschema.JsonType +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.datadog.gradle.plugin.jsonschema.TypeProperty +import com.datadog.gradle.plugin.jsonschema.variableName +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.ARRAY +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.MAP +import com.squareup.kotlinpoet.MUTABLE_MAP +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec + +class ClassGenerator( + packageName: String, + knownTypes: MutableSet +) : TypeSpecGenerator( + packageName, + knownTypes +) { + + private val deserializer = ClassJsonElementDeserializerGenerator(packageName, knownTypes) + private val stringDeserializer = ClassStringDeserializerGenerator(packageName, knownTypes) + + // region TypeSpecGenerator + + override fun generate( + definition: TypeDefinition.Class, + rootTypeName: String + ): TypeSpec.Builder { + val typeBuilder = TypeSpec.classBuilder(definition.name) + + if (definition.parentType != null) { + typeBuilder.superclass(definition.parentType.asKotlinTypeName(rootTypeName)) + } + + if ( + definition.properties.any { it.type !is TypeDefinition.Constant } || + definition.additionalProperties != null + ) { + typeBuilder.addModifiers(KModifier.DATA) + } + + typeBuilder.addKdoc(generateKDoc(definition)) + + definition.properties.forEach { + typeBuilder.addProperty(generateProperty(it, rootTypeName)) + } + if (definition.additionalProperties != null) { + typeBuilder.addProperty( + generateAdditionalProperties(definition.additionalProperties, rootTypeName) + ) + } + + if ( + definition.properties.any { it.type !is TypeDefinition.Constant } || + definition.additionalProperties != null + ) { + typeBuilder.primaryConstructor(generateConstructor(definition, rootTypeName)) + } + + typeBuilder.addFunction(generateClassSerializer(definition)) + + typeBuilder.addType(generateCompanionObject(definition, rootTypeName)) + + return typeBuilder + } + + // endregion + + // region Internal + + private fun generateKDoc(definition: TypeDefinition.Class): CodeBlock { + val docBuilder = CodeBlock.builder() + + if (definition.description.isNotBlank()) { + docBuilder.add(definition.description) + docBuilder.add("\n") + } + + definition.properties.forEach { p -> + if (p.type !is TypeDefinition.Constant && p.type.description.isNotBlank()) { + docBuilder.add("@param ${p.name.variableName()} ${p.type.description}\n") + } + } + + if ( + definition.additionalProperties != null && + definition.additionalProperties.type.description.isNotBlank() + ) { + docBuilder.add( + "@param ${Identifier.PARAM_ADDITIONAL_PROPS} ${definition.additionalProperties.type.description}\n" + ) + } + return docBuilder.build() + } + + private fun generateClassSerializer(definition: TypeDefinition.Class): FunSpec { + val funBuilder = FunSpec.builder(Identifier.FUN_TO_JSON) + .returns(ClassNameRef.JsonElement) + + if (definition.parentType != null) { + funBuilder.addModifiers(KModifier.OVERRIDE) + } + + funBuilder.addStatement("val json = %T()", ClassNameRef.JsonObject) + + definition.properties.forEach { p -> + funBuilder.appendPropertySerialization(p) + } + + if (definition.additionalProperties != null) { + funBuilder.appendAdditionalPropertiesSerialization( + definition.additionalProperties, + definition.properties.isNotEmpty() + ) + } + + funBuilder.addStatement("return json") + + return funBuilder.build() + } + + private fun FunSpec.Builder.appendPropertySerialization( + property: TypeProperty + ) { + val propertyName = property.name.variableName() + val isNullable = + property.optional && property.type !is TypeDefinition.Constant && property.type !is TypeDefinition.Null + val refName = if (isNullable) { + beginControlFlow("%L?.let·{·%LNonNull·->", propertyName, propertyName) + "${propertyName}NonNull" + } else { + propertyName + } + + when (property.type) { + is TypeDefinition.Constant -> appendConstantSerialization( + property.type, + property.name + ) + + is TypeDefinition.Primitive -> appendPrimitiveSerialization( + property, + refName + ) + + is TypeDefinition.Null -> appendNullSerialization(property) + is TypeDefinition.Array -> appendArraySerialization(property, property.type, refName) + is TypeDefinition.Class, + is TypeDefinition.OneOfClass, + is TypeDefinition.Enum -> appendTypeSerialization(property, refName) + } + + if (isNullable) { + endControlFlow() + } + } + + private fun FunSpec.Builder.appendConstantSerialization( + type: TypeDefinition.Constant, + name: String + ) { + val constantValue = type.value + if (constantValue is String || constantValue is Number) { + addStatement("json.addProperty(%S, %L)", name, name.variableName()) + } else { + error( + "Unable to generate serialization for constant $constantValue with type $type" + ) + } + } + + private fun FunSpec.Builder.appendPrimitiveSerialization( + property: TypeProperty, + propertyName: String + ) { + addStatement( + "json.addProperty(%S, %L)", + property.name, + propertyName + ) + } + + private fun FunSpec.Builder.appendNullSerialization(property: TypeProperty) { + addStatement( + "json.add(%S, %T.%L)", + property.name, + ClassNameRef.JsonNull, + "INSTANCE" + ) + } + + private fun FunSpec.Builder.appendArraySerialization( + property: TypeProperty, + propertyType: TypeDefinition.Array, + propertyName: String + ) { + val resultArrayName = "${property.name.variableName()}Array" + + addStatement( + "val %L = %T(%L.size)", + resultArrayName, + ClassNameRef.JsonArray, + propertyName + ) + + when (propertyType.items) { + is TypeDefinition.Null, + is TypeDefinition.Primitive, + is TypeDefinition.Constant -> addStatement( + "%L.forEach { %L.add(it) }", + propertyName, + resultArrayName + ) + + is TypeDefinition.Class, + is TypeDefinition.OneOfClass, + is TypeDefinition.Enum -> addStatement( + "%L.forEach { %L.add(it.%L()) }", + propertyName, + resultArrayName, + Identifier.FUN_TO_JSON + ) + + is TypeDefinition.Array -> throw UnsupportedOperationException( + "Unable to serialize an array of arrays: $propertyType" + ) + } + + addStatement("json.add(%S, %L)", property.name, resultArrayName) + } + + private fun FunSpec.Builder.appendTypeSerialization( + property: TypeProperty, + propertyName: String + ) { + addStatement( + "json.add(%S, %L.%L())", + property.name, + propertyName, + Identifier.FUN_TO_JSON + ) + } + + @Suppress("FunctionMaxLength") + private fun FunSpec.Builder.appendAdditionalPropertiesSerialization( + additionalProperties: TypeProperty, + hasKnownProperties: Boolean + ) { + beginControlFlow("%L.forEach { (k, v) ->", Identifier.PARAM_ADDITIONAL_PROPS) + + if (hasKnownProperties) { + beginControlFlow("if (k !in %L)", Identifier.PARAM_RESERVED_PROPS) + } + + when (additionalProperties.type) { + is TypeDefinition.Primitive -> addStatement("json.addProperty(k, v)") + is TypeDefinition.Class -> addStatement( + "json.add(k, %T.%L(v))", + ClassName(Identifier.PACKAGE_UTILS, Identifier.OBJECT_JSON_SERIALIZER), + Identifier.FUN_TO_JSON_ELT + ) + + is TypeDefinition.Enum -> addStatement("json.add(k, v.%L()) }", Identifier.FUN_TO_JSON) + is TypeDefinition.Null -> addStatement("json.add(k, null) }") + is TypeDefinition.Array -> error( + "Unable to generate custom serialization for Array type $additionalProperties" + ) + + is TypeDefinition.Constant -> error( + "Unable to generate custom serialization for constant type $additionalProperties" + ) + + else -> error( + "Unable to generate custom serialization for unknown type $additionalProperties" + ) + } + + if (hasKnownProperties) { + endControlFlow() + } + + endControlFlow() + } + + private fun generateConstructor( + definition: TypeDefinition.Class, + rootTypeName: String + ): FunSpec { + val constructorBuilder = FunSpec.constructorBuilder() + + definition.properties.forEach { p -> + if (p.type !is TypeDefinition.Constant) { + val propertyName = p.name.variableName() + val isNullable = (p.optional || p.type is TypeDefinition.Null) + val notNullableType = p.type.asKotlinTypeName(rootTypeName) + val propertyType = notNullableType.copy(nullable = isNullable) + constructorBuilder.addParameter( + ParameterSpec.builder(propertyName, propertyType) + .withDefaultValue(p, rootTypeName) + .build() + ) + } + } + + if (definition.additionalProperties != null) { + val mapType = definition.additionalProperties.type.asAdditionalPropertiesType( + rootTypeName, + definition.additionalProperties.readOnly + ) + constructorBuilder.addParameter( + ParameterSpec.builder(Identifier.PARAM_ADDITIONAL_PROPS, mapType) + .defaultValue(if (definition.additionalProperties.readOnly) "mapOf()" else "mutableMapOf()") + .build() + ) + } + + return constructorBuilder.build() + } + + private fun generateProperty(property: TypeProperty, rootTypeName: String): PropertySpec { + val propertyName = property.name.variableName() + val propertyType = property.type + val isNullable = (property.optional || propertyType is TypeDefinition.Null) && + (propertyType !is TypeDefinition.Constant) + val notNullableType = propertyType.asKotlinTypeName(rootTypeName) + val type = notNullableType.copy(nullable = isNullable) + val initializer = if (propertyType is TypeDefinition.Constant) { + getKotlinValue(propertyType.value, propertyType.type) + } else { + propertyName + } + + return PropertySpec.builder(propertyName, type) + .mutable(!property.readOnly) + .initializer(initializer) + .build() + } + + private fun generateAdditionalProperties( + additionalPropertyType: TypeProperty, + rootTypeName: String + ): PropertySpec { + val type = additionalPropertyType.type.asAdditionalPropertiesType(rootTypeName, additionalPropertyType.readOnly) + + return PropertySpec.builder(Identifier.PARAM_ADDITIONAL_PROPS, type) + .mutable(false) + .initializer(Identifier.PARAM_ADDITIONAL_PROPS) + .build() + } + + private fun getKotlinValue( + value: Any?, + type: Any? + ): String { + return when { + value is JsonPrimitiveType -> { + error( + "Unable to get Kotlin Value from $value with type $type" + ) + } + + value is String -> "\"$value\"" + value is Double && + (type == JsonType.INTEGER || type == JsonPrimitiveType.INTEGER) -> { + "${value.toLong()}L" + } + + value is Double -> { + "$value" + } + + value is Boolean -> { + "$value" + } + + else -> error("Unable to get Kotlin Value from $value with type $type") + } + } + + private fun generateCompanionObject( + definition: TypeDefinition.Class, + rootTypeName: String + ): TypeSpec { + val typeBuilder = TypeSpec.companionObjectBuilder() + .addFunction(stringDeserializer.generate(definition, rootTypeName)) + .addFunction(deserializer.generate(definition, rootTypeName)) + + if (definition.additionalProperties != null && definition.properties.isNotEmpty()) { + typeBuilder.addProperty(generateReservedPropertiesArray(definition)) + } + return typeBuilder.build() + } + + @Suppress("FunctionMaxLength") + private fun generateReservedPropertiesArray(definition: TypeDefinition.Class): PropertySpec { + val propertyNames = definition.properties.joinToString(", ") { "\"${it.name}\"" } + + val propertyBuilder = PropertySpec.builder( + Identifier.PARAM_RESERVED_PROPS, + ARRAY.parameterizedBy(STRING), + KModifier.INTERNAL + ).initializer("arrayOf($propertyNames)") + + return propertyBuilder.build() + } + + // endregion + + // region Internal Extensions + + private fun ParameterSpec.Builder.withDefaultValue( + p: TypeProperty, + rootTypeName: String + ): ParameterSpec.Builder { + val defaultValue = p.defaultValue + if (defaultValue != null) { + when (p.type) { + is TypeDefinition.Primitive -> defaultValue( + getKotlinValue( + defaultValue, + p.type.type + ) + ) + + is TypeDefinition.Enum -> defaultValue( + "%T.%L", + p.type.asKotlinTypeName(rootTypeName), + p.type.enumConstantName( + if (defaultValue is Number) { + defaultValue.toInt().toString() + } else { + defaultValue.toString() + } + ) + ) + + else -> throw IllegalArgumentException( + "Unable to generate default value for class: ${p.type}. " + + "This feature is not supported yet" + ) + } + } else if (p.optional || p.type is TypeDefinition.Null) { + defaultValue("null") + } + return this + } + + private fun TypeDefinition.asAdditionalPropertiesType(rootTypeName: String, readOnly: Boolean): TypeName { + val valueType = if (this is TypeDefinition.Primitive) { + this.asKotlinTypeName(rootTypeName) + } else { + ANY.copy(nullable = true) + } + return if (readOnly) { + MAP.parameterizedBy(STRING, valueType) + } else { + MUTABLE_MAP.parameterizedBy(STRING, valueType) + } + } + + // endregion +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassJsonElementDeserializerGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassJsonElementDeserializerGenerator.kt new file mode 100644 index 0000000000..d6d251340c --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassJsonElementDeserializerGenerator.kt @@ -0,0 +1,374 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.JsonType +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.datadog.gradle.plugin.jsonschema.TypeProperty +import com.datadog.gradle.plugin.jsonschema.variableName +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.jvm.throws + +class ClassJsonElementDeserializerGenerator( + packageName: String, + knownTypes: MutableSet +) : KotlinSpecGenerator( + packageName, + knownTypes +) { + + override fun generate(definition: TypeDefinition.Class, rootTypeName: String): FunSpec { + val returnType = ClassName.bestGuess(definition.name) + + val funBuilder = FunSpec.builder(Identifier.FUN_FROM_JSON_OBJ) + .addAnnotation(AnnotationSpec.builder(JvmStatic::class).build()) + .returns(returnType) + + funBuilder.throws(ClassNameRef.JsonParseException) + funBuilder.addParameter(Identifier.PARAM_JSON_OBJ, ClassNameRef.JsonObject) + funBuilder.beginControlFlow("try") + + funBuilder.appendDeserializerFunctionBlock(definition, rootTypeName) + + caughtExceptions.forEach { + funBuilder.nextControlFlow( + "catch (%L: %T)", + Identifier.CAUGHT_EXCEPTION, + it + ) + funBuilder.addStatement("throw %T(", ClassNameRef.JsonParseException) + funBuilder.addStatement(" \"$PARSE_ERROR_MSG %T\",", returnType) + funBuilder.addStatement(" %L", Identifier.CAUGHT_EXCEPTION) + funBuilder.addStatement(")") + } + funBuilder.endControlFlow() + + return funBuilder.build() + } + + // region Internal + + @Suppress("FunctionMaxLength") + private fun FunSpec.Builder.appendDeserializerFunctionBlock( + definition: TypeDefinition.Class, + rootTypeName: String + ) { + definition.properties.forEach { p -> + appendDeserializedProperty( + propertyType = p.type, + assignee = "val ${p.name.variableName()}", + getter = "${Identifier.PARAM_JSON_OBJ}.get(\"${p.name}\")", + nullable = p.optional, + rootTypeName = rootTypeName + ) + } + definition.additionalProperties?.let { + appendAdditionalPropertiesDeserialization( + it, + definition.properties.isNotEmpty(), + rootTypeName + ) + } + + definition.properties + .filter { it.type is TypeDefinition.Constant } + .forEach { appendConstantPropertyCheck(it) } + + val nonConstantProperties = definition.properties + .filter { it.type !is TypeDefinition.Constant } + .map { it.name.variableName() } + val arguments = nonConstantProperties + + definition.additionalProperties?.let { Identifier.PARAM_ADDITIONAL_PROPS } + val constructorArguments = arguments.filterNotNull().joinToString(", ") + addStatement("return %L($constructorArguments)", definition.name) + } + + private fun FunSpec.Builder.appendConstantPropertyCheck(property: TypeProperty) { + val constant = property.type as TypeDefinition.Constant + val variableName = property.name.variableName() + + if (property.optional) { + beginControlFlow("if (%L != null)", variableName) + } + + when (constant.type) { + JsonType.NULL -> { + addStatement("check(%L == null)", variableName) + } + + JsonType.BOOLEAN -> addStatement("check(%L == ${constant.value})", variableName) + JsonType.STRING -> addStatement("check(%L == %S)", variableName, constant.value) + JsonType.INTEGER -> addStatement("check(%L == ${constant.value}.toLong())", variableName) + JsonType.NUMBER -> addStatement("check(%L.toDouble() == ${constant.value})", variableName) + + JsonType.OBJECT -> TODO() + JsonType.ARRAY -> TODO() + null -> TODO() + } + if (property.optional) { + endControlFlow() + } + } + + private fun FunSpec.Builder.appendDeserializedProperty( + propertyType: TypeDefinition, + assignee: String, + getter: String, + nullable: Boolean, + rootTypeName: String + ) { + when (propertyType) { + is TypeDefinition.Null -> addStatement("$assignee = null") + is TypeDefinition.Primitive -> addStatement( + "$assignee = $getter${if (nullable) "?" else ""}.${propertyType.asPrimitiveTypeFun()}" + ) + + is TypeDefinition.Array -> appendArrayDeserialization( + propertyType, + assignee, + getter, + nullable, + rootTypeName + ) + + is TypeDefinition.Class -> { + appendObjectDeserialization( + propertyType, + assignee, + getter, + nullable, + rootTypeName + ) + } + + is TypeDefinition.OneOfClass -> { + appendObjectDeserialization( + propertyType, + assignee, + getter, + nullable, + rootTypeName + ) + } + + is TypeDefinition.Enum -> appendEnumDeserialization( + propertyType, + assignee, + getter, + nullable, + rootTypeName + ) + + is TypeDefinition.Constant -> addStatement( + "$assignee = $getter${if (nullable) "?" else ""}.${propertyType.asPrimitiveTypeFun()}" + ) + } + } + + private fun FunSpec.Builder.appendArrayDeserialization( + arrayType: TypeDefinition.Array, + assignee: String, + getter: String, + nullable: Boolean, + rootTypeName: String + ) { + val opt = if (nullable) "?" else "" + beginControlFlow( + "$assignee = $getter$opt.asJsonArray$opt.let·{·%L·->", + Identifier.PARAM_JSON_ARRAY + ) + val collectionClassName: ClassName = if (arrayType.uniqueItems) { + ClassNameRef.MutableSet + } else { + ClassNameRef.MutableList + } + addStatement( + "val %L = %T(%L.size())", + Identifier.PARAM_COLLECTION, + collectionClassName.parameterizedBy( + arrayType.items.asKotlinTypeName(rootTypeName) + ), + Identifier.PARAM_JSON_ARRAY + ) + beginControlFlow("%L.forEach", Identifier.PARAM_JSON_ARRAY) + appendArrayItemDeserialization(arrayType, rootTypeName) + + endControlFlow() + addStatement("%L", Identifier.PARAM_COLLECTION) + endControlFlow() + } + + private fun FunSpec.Builder.appendArrayItemDeserialization( + arrayType: TypeDefinition.Array, + rootTypeName: String + ) { + when (arrayType.items) { + is TypeDefinition.Primitive -> addStatement( + "%L.add(it.${arrayType.items.asPrimitiveTypeFun()})", + Identifier.PARAM_COLLECTION + ) + + is TypeDefinition.OneOfClass, + is TypeDefinition.Class -> addStatement( + "%L.add(%T.%L(it.asJsonObject))", + Identifier.PARAM_COLLECTION, + arrayType.items.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON_OBJ + ) + + is TypeDefinition.Enum -> addStatement( + "%L.add(%T.%L(it.asString))", + Identifier.PARAM_COLLECTION, + arrayType.items.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON + ) + + else -> error( + "Unable to deserialize an array of ${arrayType.items}" + ) + } + } + + private fun FunSpec.Builder.appendObjectDeserialization( + propertyType: TypeDefinition.Class, + assignee: String, + getter: String, + nullable: Boolean, + rootTypeName: String + ) { + val opt = if (nullable) "?" else "" + beginControlFlow("$assignee = $getter$opt.asJsonObject$opt.let") + addStatement( + "%T.%L(it)", + propertyType.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON_OBJ + ) + endControlFlow() + } + + private fun FunSpec.Builder.appendObjectDeserialization( + propertyType: TypeDefinition.OneOfClass, + assignee: String, + getter: String, + nullable: Boolean, + rootTypeName: String + ) { + val opt = if (nullable) "?" else "" + beginControlFlow("$assignee = $getter$opt.asJsonObject$opt.let") + + addStatement( + "%T.%L(it)", + propertyType.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON_OBJ + ) + endControlFlow() + } + + private fun FunSpec.Builder.appendEnumDeserialization( + propertyType: TypeDefinition.Enum, + assignee: String, + getter: String, + nullable: Boolean, + rootTypeName: String + ) { + if (propertyType.allowsNull()) { + val elementName = "json${propertyType.name.variableName()}" + addStatement("val $elementName = $getter") + beginControlFlow( + "$assignee = if ($elementName is %T || $elementName == null)", + ClassNameRef.JsonNull + ) + addStatement( + "%T.%L(null)", + propertyType.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON + ) + nextControlFlow("else") + addStatement( + "%T.%L($elementName.asString)", + propertyType.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON + ) + endControlFlow() + } else if (nullable) { + beginControlFlow("$assignee = $getter?.asString?.let") + addStatement( + "%T.%L(it)", + propertyType.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON + ) + endControlFlow() + } else { + addStatement( + "$assignee = %T.%L($getter.asString)", + propertyType.asKotlinTypeName(rootTypeName), + Identifier.FUN_FROM_JSON + ) + } + } + + @Suppress("FunctionMaxLength") + private fun FunSpec.Builder.appendAdditionalPropertiesDeserialization( + additionalProperties: TypeProperty, + hasKnownProperties: Boolean, + rootTypeName: String + ) { + addStatement( + "val %L = mutableMapOf<%T, %T>()", + Identifier.PARAM_ADDITIONAL_PROPS, + STRING, + additionalProperties.type.additionalPropertyTypeName(rootTypeName) + ) + beginControlFlow( + "for (entry in %L.entrySet())", + Identifier.PARAM_JSON_OBJ + ) + + if (hasKnownProperties) { + beginControlFlow( + "if (entry.key !in %L)", + Identifier.PARAM_RESERVED_PROPS + ) + } + + if (additionalProperties.type is TypeDefinition.Class) { + addStatement( + "%L[entry.key] = entry.value", + Identifier.PARAM_ADDITIONAL_PROPS + ) + } else { + appendDeserializedProperty( + propertyType = additionalProperties.type, + assignee = "${Identifier.PARAM_ADDITIONAL_PROPS}[entry.key]", + getter = "entry.value", + nullable = false, + rootTypeName = rootTypeName + ) + } + + if (hasKnownProperties) { + endControlFlow() + } + endControlFlow() + } + + // endregion + + companion object { + private const val PARSE_ERROR_MSG = "Unable to parse json into type" + + private val caughtExceptions = arrayOf( + ClassNameRef.IllegalStateException, + ClassNameRef.NumberFormatException, + ClassNameRef.NullPointerException + ) + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassNameRef.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassNameRef.kt new file mode 100644 index 0000000000..4e624efcda --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassNameRef.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.squareup.kotlinpoet.ClassName + +object ClassNameRef { + val JsonArray = ClassName.bestGuess("com.google.gson.JsonArray") + val JsonElement = ClassName.bestGuess("com.google.gson.JsonElement") + val JsonNull = ClassName.bestGuess("com.google.gson.JsonNull") + val JsonObject = ClassName.bestGuess("com.google.gson.JsonObject") + val JsonParser = ClassName.bestGuess("com.google.gson.JsonParser") + val JsonParseException = ClassName.bestGuess("com.google.gson.JsonParseException") + val JsonPrimitive = ClassName.bestGuess("com.google.gson.JsonPrimitive") + val IllegalStateException = ClassName.bestGuess("java.lang.IllegalStateException") + val NumberFormatException = ClassName.bestGuess("java.lang.NumberFormatException") + val NullPointerException = ClassName.bestGuess("java.lang.NullPointerException") + val MutableList = ClassName.bestGuess("kotlin.collections.ArrayList") + val MutableSet = ClassName.bestGuess("kotlin.collections.HashSet") +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassStringDeserializerGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassStringDeserializerGenerator.kt new file mode 100644 index 0000000000..a1a4b2cb09 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/ClassStringDeserializerGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.jvm.throws + +class ClassStringDeserializerGenerator( + packageName: String, + knownTypes: MutableSet +) : KotlinSpecGenerator( + packageName, + knownTypes +) { + override fun generate(definition: TypeDefinition.Class, rootTypeName: String): FunSpec { + val isConstantClass = definition.isConstantClass() + val returnType = ClassName.bestGuess(definition.name) + + val funBuilder = FunSpec.builder(Identifier.FUN_FROM_JSON) + .addAnnotation(AnnotationSpec.builder(JvmStatic::class).build()) + .returns(returnType) + + funBuilder.throws(ClassNameRef.JsonParseException) + funBuilder.addParameter(Identifier.PARAM_JSON_STR, STRING) + funBuilder.beginControlFlow("try") + + funBuilder.addStatement( + "val %L = %T.parseString(%L).asJsonObject", + Identifier.PARAM_JSON_OBJ, + ClassNameRef.JsonParser, + Identifier.PARAM_JSON_STR + ) + funBuilder.addStatement("return %L(%L)", Identifier.FUN_FROM_JSON_OBJ, Identifier.PARAM_JSON_OBJ) + + caughtExceptions.forEach { + funBuilder.nextControlFlow( + "catch (%L: %T)", + Identifier.CAUGHT_EXCEPTION, + it + ) + funBuilder.addStatement("throw %T(", ClassNameRef.JsonParseException) + funBuilder.addStatement(" \"$PARSE_ERROR_MSG %T\",", returnType) + funBuilder.addStatement(" %L", Identifier.CAUGHT_EXCEPTION) + funBuilder.addStatement(")") + } + funBuilder.endControlFlow() + + return funBuilder.build() + } + + companion object { + private const val PARSE_ERROR_MSG = "Unable to parse json into type" + + private val caughtExceptions = arrayOf( + ClassNameRef.IllegalStateException + ) + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/EnumClassGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/EnumClassGenerator.kt new file mode 100644 index 0000000000..b572f82d65 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/EnumClassGenerator.kt @@ -0,0 +1,146 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.JsonType +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asTypeName + +class EnumClassGenerator( + packageName: String, + knownTypes: MutableSet +) : TypeSpecGenerator( + packageName, + knownTypes +) { + + // region TypeSpecGenerator + + override fun generate( + definition: TypeDefinition.Enum, + rootTypeName: String + ): TypeSpec.Builder { + val enumBuilder = TypeSpec.enumBuilder(definition.name) + + val jsonValueType = definition.jsonValueType() + val typeName = jsonValueType.asTypeName().copy(nullable = definition.allowsNull()) + + enumBuilder.addKdoc(generateKDoc(definition)) + + enumBuilder.primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + Identifier.PARAM_JSON_VALUE, + typeName + ) + .build() + ) + + enumBuilder.addProperty( + PropertySpec.builder(Identifier.PARAM_JSON_VALUE, typeName, KModifier.PRIVATE) + .initializer(Identifier.PARAM_JSON_VALUE) + .build() + ) + + val parameterFormat = if (definition.type == JsonType.NUMBER) "%L" else "%S" + definition.values.forEach { value -> + val enumValue = if (value == null) { + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("null") + .build() + } else { + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter(parameterFormat, value) + .build() + } + enumBuilder.addEnumConstant(definition.enumConstantName(value), enumValue) + } + + enumBuilder.addFunction(generateEnumSerializer(definition)) + + enumBuilder.addType(generateCompanionObject(definition, rootTypeName)) + + return enumBuilder + } + + // endregion + + // region Internal + + private fun generateKDoc(definition: TypeDefinition.Enum): CodeBlock { + val docBuilder = CodeBlock.builder() + + if (definition.description.isNotBlank()) { + docBuilder.add(definition.description) + docBuilder.add("\n") + } + return docBuilder.build() + } + + private fun generateCompanionObject( + definition: TypeDefinition.Enum, + rootTypeName: String + ): TypeSpec { + return TypeSpec.companionObjectBuilder() + .addFunction(generateEnumDeserializer(definition, rootTypeName)) + .build() + } + + private fun generateEnumSerializer(definition: TypeDefinition.Enum): FunSpec { + val funBuilder = FunSpec.builder(Identifier.FUN_TO_JSON) + .returns(ClassNameRef.JsonElement) + + if (definition.allowsNull()) { + funBuilder.beginControlFlow("if (%L == null)", Identifier.PARAM_JSON_VALUE) + funBuilder.addStatement("return %T.%L", ClassNameRef.JsonNull, "INSTANCE") + funBuilder.nextControlFlow("else") + } + + funBuilder.addStatement( + "return %T(%L)", + ClassNameRef.JsonPrimitive, + Identifier.PARAM_JSON_VALUE + ) + + if (definition.allowsNull()) { + funBuilder.endControlFlow() + } + + return funBuilder.build() + } + + private fun generateEnumDeserializer( + definition: TypeDefinition.Enum, + rootTypeName: String + ): FunSpec { + val funBuilder = FunSpec.builder(Identifier.FUN_FROM_JSON) + .addAnnotation(AnnotationSpec.builder(JvmStatic::class).build()) + .addParameter( + Identifier.PARAM_JSON_STR, + String::class.asTypeName().copy(nullable = definition.allowsNull()) + ) + .returns(definition.asKotlinTypeName(rootTypeName)) + + funBuilder.beginControlFlow("return values().first") + funBuilder.addStatement( + if (definition.type == JsonType.NUMBER) "it.%L.toString() == %L" else "it.%L == %L", + Identifier.PARAM_JSON_VALUE, + Identifier.PARAM_JSON_STR + ) + funBuilder.endControlFlow() + + return funBuilder.build() + } + + // endregion +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/FileGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/FileGenerator.kt new file mode 100644 index 0000000000..f2045213ae --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/FileGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.TypeSpec +import org.gradle.api.logging.Logger +import java.io.File + +class FileGenerator( + private val outputDir: File, + private val packageName: String, + private val logger: Logger +) { + + private val knownTypes: MutableSet = mutableSetOf() + + private val classGenerator = ClassGenerator(packageName, knownTypes) + private val enumGenerator = EnumClassGenerator(packageName, knownTypes) + private val multiClassGenerator = MultiClassGenerator(classGenerator, packageName, knownTypes) + + // region FileGenerator + + /** + * Generate a Kotlin file based on the input schema file + */ + fun generate(typeDefinition: TypeDefinition) { + logger.info("Generating class for type $typeDefinition with package name $packageName") + knownTypes.clear() + generateFile(typeDefinition) + } + + // endregion + + // region Internal + + private val isUnwrittenClass: (KotlinTypeWrapper) -> Boolean = { k -> + (k.type is TypeDefinition.Class || k.type is TypeDefinition.OneOfClass) && + !k.written + } + private val isEnum: (KotlinTypeWrapper) -> Boolean = { k -> + (k.type is TypeDefinition.Enum) && !k.written + } + + /** + * Generate a Kotlin file based on the root schema definition + */ + private fun generateFile(definition: TypeDefinition) { + val rootTypeName = when (definition) { + is TypeDefinition.Class -> definition.name + is TypeDefinition.OneOfClass -> definition.name + else -> error("Top level type $definition is not supported") + } + + val fileBuilder = FileSpec.builder(packageName, rootTypeName) + + knownTypes.add( + KotlinTypeWrapper( + rootTypeName, + ClassName(packageName, rootTypeName), + definition + ).apply { written = true } + ) + val topLevelTypeBuilder = generateTypeSpec(definition, rootTypeName) + + while (knownTypes.any(isUnwrittenClass)) { + val nestedClasses = knownTypes.filter(isUnwrittenClass).toSet() + nestedClasses.forEach { + topLevelTypeBuilder.addType(generateTypeSpec(it.type, rootTypeName).build()) + it.written = true + } + } + + while (knownTypes.any(isEnum)) { + val nestedEnums = knownTypes.filter(isEnum).toSet() + nestedEnums.forEach { + topLevelTypeBuilder.addType(generateTypeSpec(it.type, rootTypeName).build()) + it.written = true + } + } + + fileBuilder.addType(topLevelTypeBuilder.build()) + fileBuilder + .indent(" ") + .build() + .writeTo(outputDir) + } + + private fun generateTypeSpec( + definition: TypeDefinition, + rootTypeName: String + ): TypeSpec.Builder { + return when (definition) { + is TypeDefinition.Class -> classGenerator.generate(definition, rootTypeName) + is TypeDefinition.Enum -> enumGenerator.generate(definition, rootTypeName) + is TypeDefinition.OneOfClass -> multiClassGenerator.generate(definition, rootTypeName) + else -> throw IllegalArgumentException("Can't generate a file for type $definition") + } + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/Identifier.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/Identifier.kt new file mode 100644 index 0000000000..8c1a090e4a --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/Identifier.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +object Identifier { + + const val FUN_TO_JSON = "toJson" + const val OBJECT_JSON_SERIALIZER = "JsonSerializer" + const val FUN_TO_JSON_ELT = "toJsonElement" + const val FUN_FROM_JSON = "fromJson" + const val FUN_FROM_JSON_OBJ = "fromJsonObject" + + const val PARAM_JSON_STR = "jsonString" + const val PARAM_JSON_ARRAY = "jsonArray" + const val PARAM_JSON_OBJ = "jsonObject" + const val PARAM_JSON_VALUE = "jsonValue" + const val PARAM_ADDITIONAL_PROPS = "additionalProperties" + const val PARAM_COLLECTION = "collection" + + const val PARAM_RESERVED_PROPS = "RESERVED_PROPERTIES" + + const val CAUGHT_EXCEPTION = "e" + + const val PACKAGE_UTILS = "com.datadog.android.core.internal.utils" +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/KotlinSpecGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/KotlinSpecGenerator.kt new file mode 100644 index 0000000000..955f1728d4 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/KotlinSpecGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.datadog.gradle.plugin.jsonschema.asKotlinTypeName +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.NOTHING +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.SET +import com.squareup.kotlinpoet.TypeName + +abstract class KotlinSpecGenerator( + val packageName: String, + val knownTypes: MutableSet +) { + + /** + * Generates a KotlinPoet Spec based on the provided Json information + * @param rootTypeName the name of the root type (matching the name of the file) + * @param definition the definition read from a Json Schema file + */ + abstract fun generate(definition: I, rootTypeName: String): O + + // region Utilities + + protected fun TypeDefinition.name(): String? { + return when (this) { + is TypeDefinition.Array, + is TypeDefinition.Primitive, + is TypeDefinition.Constant, + is TypeDefinition.Null -> null + is TypeDefinition.Enum -> name + is TypeDefinition.Class -> name + is TypeDefinition.OneOfClass -> name + } + } + + protected fun TypeDefinition.asKotlinTypeName( + rootTypeName: String + ): TypeName { + return when (this) { + is TypeDefinition.Null -> NOTHING + is TypeDefinition.Primitive -> type.asKotlinTypeName() + is TypeDefinition.Constant -> type.asKotlinTypeName() + is TypeDefinition.Array -> { + if (uniqueItems) { + SET.parameterizedBy(items.asKotlinTypeName(rootTypeName)) + } else { + LIST.parameterizedBy(items.asKotlinTypeName(rootTypeName)) + } + } + is TypeDefinition.Class -> withUniqueTypeName(rootTypeName).typeName + is TypeDefinition.Enum -> withUniqueTypeName(rootTypeName).typeName + is TypeDefinition.OneOfClass -> withUniqueTypeName(rootTypeName).typeName + } + } + + fun TypeDefinition.additionalPropertyTypeName(rootTypeName: String): TypeName { + return if (this is TypeDefinition.Primitive) { + this.asKotlinTypeName(rootTypeName) + } else { + ANY.copy(nullable = true) + } + } + + fun TypeDefinition.Class.withUniqueTypeName(rootTypeName: String): KotlinTypeWrapper { + val matchingClass = knownTypes.firstOrNull { + it.type.matches(this) + } + return if (matchingClass == null) { + val uniqueName = name.uniqueTypeName() + val typeName = if ((parentType == null) || (parentType.name == rootTypeName)) { + ClassName(packageName, rootTypeName, uniqueName) + } else { + ClassName(packageName, rootTypeName, parentType.name, uniqueName) + } + KotlinTypeWrapper( + uniqueName, + typeName, + this.copy(name = uniqueName) + ).apply { knownTypes.add(this) } + } else { + matchingClass + } + } + + private fun TypeDefinition.Enum.withUniqueTypeName(rootTypeName: String): KotlinTypeWrapper { + val matchingEnum = knownTypes.firstOrNull { + it.type.matches(this) + } + return if (matchingEnum == null) { + val uniqueName = name.uniqueTypeName() + val typeName = ClassName(packageName, rootTypeName, uniqueName) + KotlinTypeWrapper( + uniqueName, + typeName, + this.copy(name = uniqueName) + ).apply { knownTypes.add(this) } + } else { + matchingEnum + } + } + + private fun TypeDefinition.OneOfClass.withUniqueTypeName(rootTypeName: String): KotlinTypeWrapper { + val matchingOneOf = knownTypes.firstOrNull { + it.type.matches(this) + } + return if (matchingOneOf == null) { + val uniqueName = name.uniqueTypeName() + val typeName = ClassName(packageName, rootTypeName, uniqueName) + KotlinTypeWrapper( + uniqueName, + typeName, + this.copy(name = uniqueName) + ).apply { knownTypes.add(this) } + } else { + matchingOneOf + } + } + + private fun String.uniqueTypeName(): String { + var uniqueName = this + var tries = 0 + while (knownTypes.any { it.name == uniqueName }) { + tries++ + uniqueName = "${this}$tries" + } + return uniqueName + } + + // endregion +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/KotlinTypeWrapper.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/KotlinTypeWrapper.kt new file mode 100644 index 0000000000..b4d23efb04 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/KotlinTypeWrapper.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.squareup.kotlinpoet.TypeName + +data class KotlinTypeWrapper( + val name: String, + val typeName: TypeName, + val type: TypeDefinition +) { + var written: Boolean = false +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/MultiClassGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/MultiClassGenerator.kt new file mode 100644 index 0000000000..93b52a0745 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/MultiClassGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.jvm.throws + +class MultiClassGenerator( + val classGenerator: KotlinSpecGenerator, + packageName: String, + knownTypes: MutableSet +) : TypeSpecGenerator( + packageName, + knownTypes +) { + + //region TypeSpecGenerator + + override fun generate( + definition: TypeDefinition.OneOfClass, + rootTypeName: String + ): TypeSpec.Builder { + val typeBuilder = TypeSpec.classBuilder(definition.name) + .addModifiers(KModifier.SEALED) + + if (definition.description.isNotBlank()) { + val docBuilder = CodeBlock.builder() + docBuilder.add(definition.description) + docBuilder.add("\n") + typeBuilder.addKdoc(docBuilder.build()) + } + + definition.options.forEach { + when (it) { + is TypeDefinition.Class -> { + val childType = it.copy(parentType = definition) + val wrapper = childType.withUniqueTypeName(rootTypeName) + typeBuilder.addType( + classGenerator.generate(childType, rootTypeName).build() + ) + wrapper.written = true + } + else -> error( + "Can't have type $it as child of a `one_of` block" + ) + } + } + + typeBuilder.addFunction(generateMultiClassSerializer()) + + typeBuilder.addType(generateCompanionObject(definition, rootTypeName)) + + return typeBuilder + } + + // endregion + + // region Internal + + private fun generateMultiClassSerializer(): FunSpec { + return FunSpec.builder(Identifier.FUN_TO_JSON) + .addModifiers(KModifier.ABSTRACT) + .returns(ClassNameRef.JsonElement).build() + } + + private fun generateCompanionObject( + definition: TypeDefinition.OneOfClass, + rootTypeName: String + ): TypeSpec { + return TypeSpec.companionObjectBuilder() + .addFunction(generateMultiClassStringDeserializer(definition)) + .addFunction(generateMultiClassDeserializer(definition, rootTypeName)) + .build() + } + + @Suppress("FunctionMaxLength") + private fun generateMultiClassStringDeserializer( + definition: TypeDefinition.OneOfClass + ): FunSpec { + val returnType = ClassName.bestGuess(definition.name) + + val funBuilder = FunSpec.builder(Identifier.FUN_FROM_JSON) + .addAnnotation(AnnotationSpec.builder(JvmStatic::class).build()) + .throws(ClassNameRef.JsonParseException) + .addParameter(Identifier.PARAM_JSON_STR, STRING) + .returns(returnType) + + funBuilder.beginControlFlow("try") + + funBuilder.addStatement( + "val %L = %T.parseString(%L).asJsonObject", + Identifier.PARAM_JSON_OBJ, + ClassNameRef.JsonParser, + Identifier.PARAM_JSON_STR + ) + funBuilder.addStatement( + "return %L(%L)", + Identifier.FUN_FROM_JSON_OBJ, + Identifier.PARAM_JSON_OBJ + ) + + funBuilder.nextControlFlow( + "catch (%L: %T)", + Identifier.CAUGHT_EXCEPTION, + ClassNameRef.IllegalStateException + ) + funBuilder.addStatement("throw %T(", ClassNameRef.JsonParseException) + funBuilder.addStatement(" \"$PARSE_ERROR_MSG %T\",", returnType) + funBuilder.addStatement(" %L", Identifier.CAUGHT_EXCEPTION) + funBuilder.addStatement(")") + funBuilder.endControlFlow() + + return funBuilder.build() + } + + private fun generateMultiClassDeserializer( + definition: TypeDefinition.OneOfClass, + rootTypeName: String + ): FunSpec { + val returnType = definition.asKotlinTypeName(rootTypeName) + val funBuilder = FunSpec.builder(Identifier.FUN_FROM_JSON_OBJ) + .addAnnotation(AnnotationSpec.builder(JvmStatic::class).build()) + .throws(ClassNameRef.JsonParseException) + .addParameter(Identifier.PARAM_JSON_OBJ, ClassNameRef.JsonObject) + .returns(returnType) + + // create error variable + funBuilder.addStatement("val errors = mutableListOf()") + + // try to parse against all possible types + val options = mutableListOf() + definition.options.forEach { + val typeName = it.asKotlinTypeName(rootTypeName) + val variableName = "as${it.name()}" + funBuilder.beginControlFlow("val %L = try", variableName) + funBuilder.addStatement( + "%T.%L(%L)", + typeName, + Identifier.FUN_FROM_JSON_OBJ, + Identifier.PARAM_JSON_OBJ + ) + funBuilder.nextControlFlow( + "catch (%L: %T)", + Identifier.CAUGHT_EXCEPTION, + ClassNameRef.JsonParseException + ) + funBuilder.addStatement( + "errors.add(%L)", + Identifier.CAUGHT_EXCEPTION + ) + funBuilder.addStatement("null") + funBuilder.endControlFlow() + options.add(variableName) + } + + funBuilder.addStatement("val result = arrayOf(") + options.forEach { funBuilder.addStatement(" $it,") } + funBuilder.addStatement(").firstOrNull { it != null }") + + funBuilder.beginControlFlow("if (result == null)") + funBuilder.addStatement("val message = \"$PARSE_ERROR_MSG \\n\" + \"%T\\n\" +", returnType) + funBuilder.addStatement(" errors.joinToString(\"\\n\") { it.message.toString() }") + funBuilder.addStatement("throw %T(message)", ClassNameRef.JsonParseException) + funBuilder.endControlFlow() + funBuilder.addStatement("return result") + + return funBuilder.build() + } + + // endregion + + companion object { + private const val PARSE_ERROR_MSG = "Unable to parse json into one of type" + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/TypeSpecGenerator.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/TypeSpecGenerator.kt new file mode 100644 index 0000000000..7732f49e76 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/jsonschema/generator/TypeSpecGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.squareup.kotlinpoet.TypeSpec + +abstract class TypeSpecGenerator( + packageName: String, + knownTypes: MutableSet +) : KotlinSpecGenerator( + packageName, + knownTypes +) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/CheckTransitiveDependenciesTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/CheckTransitiveDependenciesTask.kt new file mode 100644 index 0000000000..aacf5f5a65 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/CheckTransitiveDependenciesTask.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.transdeps + +import com.datadog.gradle.plugin.CheckGeneratedFileTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import java.io.File +import javax.inject.Inject + +open class CheckTransitiveDependenciesTask @Inject constructor( + execOperations: ExecOperations +) : CheckGeneratedFileTask( + genTaskName = TransitiveDependenciesPlugin.TASK_GEN_TRANSITIVE_DEPS, + execOperations +) { + + @InputFile + lateinit var dependenciesFile: File + + init { + group = "datadog" + description = "Check the transitive dependencies of the library" + } + + // region Task + + @TaskAction + fun applyTask() { + verifyGeneratedFileExists(dependenciesFile) + } + + @InputFile + fun getInputFile() = dependenciesFile + + // endregion +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/GenerateTransitiveDependenciesTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/GenerateTransitiveDependenciesTask.kt new file mode 100644 index 0000000000..a920a65650 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/GenerateTransitiveDependenciesTask.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.transdeps + +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import java.io.File + +open class GenerateTransitiveDependenciesTask : DefaultTask() { + + @get:Input + var humanReadableSize: Boolean = true + + @get:Input + var sortByName: Boolean = true + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + val libsVersionCatalog: File + get() = project.rootProject.layout.projectDirectory.file("gradle/libs.versions.toml").asFile + + @get: OutputFile + lateinit var dependenciesFile: File + + init { + group = "datadog" + description = "Generate the list of transitive dependencies of the library" + } + + // region Task + + @TaskAction + fun applyTask() { + dependenciesFile.writeText("Dependencies List\n\n") + val implementation = project.configurations.getByName("releaseCompileClasspath") + listConfigurationDependencies(implementation) + } + + // endregion + + // region Internal + + private fun listConfigurationDependencies(configuration: Configuration) { + check(configuration.isCanBeResolved) { "$configuration cannot be resolved" } + + val sortedArtifacts = if (sortByName) { + configuration.incoming + .artifactView { + componentFilter { it !is ProjectComponentIdentifier } + } + .files + .sortedBy { it.absolutePath } + } else { + configuration.sortedBy { -it.length() } + } + + var sum = 0L + sortedArtifacts.forEach { + sum += it.length() + dependenciesFile.appendText(getDependencyFileDescription(it)) + } + + dependenciesFile.appendText("\n${TOTAL.padEnd(PADDING)}:${size(sum)}\n\n") + } + + private fun getDependencyFileDescription(it: File): String { + val hash = it.parentFile + val version = hash.parentFile + val artifact = version.parentFile + val group = artifact.parentFile + + val title = "${group.name}:${artifact.name}:${version.name}" + + return "${title.padEnd(PADDING)}:${size(it.length())}\n" + } + + private fun size(size: Long): String { + if (humanReadableSize) { + val rawSize = when { + size >= 2 * MB -> "${size / MB} Mb" + size >= 2 * KB -> "${size / KB} Kb" + else -> "$size b " + } + return rawSize.padStart(8) + } else { + return "$size b ".padStart(16) + } + } + + // endregion + + companion object { + private const val PADDING = 64 + private const val KB = 1024 + private const val MB = 1024 * 1024 + + private const val TOTAL = "Total transitive dependencies size" + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/TransitiveDependenciesPlugin.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/TransitiveDependenciesPlugin.kt index 1574680082..957f3d84eb 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/TransitiveDependenciesPlugin.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/TransitiveDependenciesPlugin.kt @@ -7,26 +7,32 @@ package com.datadog.gradle.plugin.transdeps import com.datadog.gradle.config.taskConfig -import java.io.File import org.gradle.api.Plugin import org.gradle.api.Project import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.File class TransitiveDependenciesPlugin : Plugin { override fun apply(target: Project) { + target.tasks.register(TASK_GEN_TRANSITIVE_DEPS, GenerateTransitiveDependenciesTask::class.java) { + dependenciesFile = File(target.projectDir, FILE_NAME) + } - val task = target.tasks.create(TASK_NAME, TransitiveDependenciesTask::class.java) - task.outputFile = File(target.projectDir, FILE_NAME) + target.tasks.register(TASK_CHECK_TRANSITIVE_DEPS, CheckTransitiveDependenciesTask::class.java) { + dependenciesFile = File(target.projectDir, FILE_NAME) + dependsOn(TASK_GEN_TRANSITIVE_DEPS) + } target.taskConfig { - finalizedBy(TASK_NAME) + finalizedBy(TASK_GEN_TRANSITIVE_DEPS) } } companion object { - const val TASK_NAME = "listTransitiveDependencies" + const val TASK_GEN_TRANSITIVE_DEPS = "generateTransitiveDependenciesList" + const val TASK_CHECK_TRANSITIVE_DEPS = "checkTransitiveDependenciesList" const val FILE_NAME = "transitiveDependencies" } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/TransitiveDependenciesTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/TransitiveDependenciesTask.kt deleted file mode 100644 index e508db8d59..0000000000 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/transdeps/TransitiveDependenciesTask.kt +++ /dev/null @@ -1,100 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.transdeps - -import java.io.File -import org.gradle.api.DefaultTask -import org.gradle.api.artifacts.Configuration -import org.gradle.api.artifacts.ProjectDependency -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction - -open class TransitiveDependenciesTask : DefaultTask() { - - @get:Input - var humanReadableSize: Boolean = true - - @get:Input - var sortByName: Boolean = true - - @get: OutputFile - lateinit var outputFile: File - - init { - group = "datadog" - description = "Generate the list of transitive dependencies of the library" - } - - // region Task - - @TaskAction - fun applyTask() { - outputFile.writeText("Dependencies List\n\n") - val implementation = project.configurations.getByName("releaseCompileClasspath") - listConfigurationDependencies(implementation) - } - - // endregion - - // region Internal - - private fun listConfigurationDependencies(configuration: Configuration) { - check(configuration.isCanBeResolved) { "$configuration cannot be resolved" } - - val sortedArtifacts = if (sortByName) { - configuration.files { - // ProjectDependency (i.e. local modules) don't have a file associated - it !is ProjectDependency - }.sortedBy { it.absolutePath } - } else { - configuration.sortedBy { -it.length() } - } - - var sum = 0L - sortedArtifacts.forEach { - sum += it.length() - outputFile.appendText(getDependencyFileDescription(it)) - } - - outputFile.appendText("\n${TOTAL.padEnd(PADDING)}:${size(sum)}\n\n") - } - - private fun getDependencyFileDescription(it: File): String { - val hash = it.parentFile - val version = hash.parentFile - val artifact = version.parentFile - val group = artifact.parentFile - - val title = "${group.name}:${artifact.name}:${version.name}" - - return "${title.padEnd(PADDING)}:${size(it.length())}\n" - } - - private fun size(size: Long): String { - if (humanReadableSize) { - val rawSize = when { - size >= 2 * MB -> "${size / MB} Mb" - size >= 2 * KB -> "${size / KB} Kb" - else -> "$size b " - } - return rawSize.padStart(8) - } else { - return "$size b ".padStart(16) - } - } - - // endregion - - companion object { - private const val PADDING = 64 - private const val KB = 1024 - private const val MB = 1024 * 1024 - - private const val TOTAL = "Total transitive dependencies size" - } -} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/verification/GenerateVerificationXmlTask.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/verification/GenerateVerificationXmlTask.kt new file mode 100644 index 0000000000..97c568dcd7 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/verification/GenerateVerificationXmlTask.kt @@ -0,0 +1,121 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.verification + +import com.datadog.gradle.config.AndroidConfig +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.redundent.kotlin.xml.PrintOptions +import org.redundent.kotlin.xml.xml +import java.io.File +import java.security.MessageDigest + +open class GenerateVerificationXmlTask : DefaultTask() { + + init { + group = "datadog" + description = + "Generate the verification-metadata.xml for the artifact built from the module" + } + + private val md = MessageDigest.getInstance("SHA-256") + + // region Task + + @TaskAction + fun applyTask() { + val buildDir = project.layout.buildDirectory.asFile.get() + val projectDir = project.layout.projectDirectory.asFile + val publicationReleaseDir = File(File(buildDir, "publications"), "release") + val outputFile = File(projectDir, VerificationXmlPlugin.XML_FILE_NAME) + + val aarFile = File(File(File(buildDir, "outputs"), "aar"), "${project.name}-release.aar") + val pomFile = File(publicationReleaseDir, "pom-default.xml") + val moduleFile = File(publicationReleaseDir, "module.json") + + val filesWithExt = mapOf( + aarFile to "aar", + pomFile to "pom", + moduleFile to "module" + ) + + val publicKey = System.getenv("GPG_PUBLIC_FINGERPRINT") + val hasPublicKey = !publicKey.isNullOrBlank() + + val content = xml(TAG_ROOT) { + xmlns = NS_DEPS_VERIF + TAG_CONFIGURATION { + TAG_VERIF_METADATA { text(true.toString()) } + TAG_VERIF_SIGNATURES { text(hasPublicKey.toString()) } + } + TAG_COMPONENTS { + TAG_COMPONENT { + attribute(ATTR_GROUP, project.group) + attribute(ATTR_NAME, project.name) + attribute(ATTR_VERSION, AndroidConfig.VERSION.name) + + filesWithExt.forEach { (file, ext) -> + TAG_ARTIFACT { + attribute(ATTR_NAME, "${project.name}-${AndroidConfig.VERSION.name}.$ext") + TAG_SHA256 { + attribute(ATTR_VALUE, file.sha256()) + attribute(ATTR_ORIGIN, ORIGIN) + } + if (hasPublicKey) { + TAG_PGP { + attribute(ATTR_VALUE, publicKey) + } + } + } + } + } + } + } + val xmlContent = content.toString( + PrintOptions( + indent = " ", + pretty = true, + singleLineTextElements = true + ) + ) + outputFile.writeText(XML_PREFIX + xmlContent, Charsets.UTF_8) + } + + // endregion + + // region Internal + + fun File.sha256(): String { + return md.digest(readBytes()).fold("", { str, byte -> str + "%02x".format(byte) }) + } + + // endregion + + companion object { + + private const val NS_DEPS_VERIF = "https://schema.gradle.org/dependency-verification" + private const val XML_PREFIX = "\n" + + private const val TAG_ROOT = "verification-metadata" + private const val TAG_CONFIGURATION = "configuration" + private const val TAG_VERIF_METADATA = "verify-metadata" + private const val TAG_VERIF_SIGNATURES = "verify-signatures" + private const val TAG_COMPONENTS = "components" + private const val TAG_COMPONENT = "component" + private const val TAG_SHA256 = "sha256" + private const val TAG_PGP = "pgp" + + private const val TAG_ARTIFACT = "artifact" + private const val ATTR_GROUP = "group" + private const val ATTR_NAME = "name" + private const val ATTR_VERSION = "version" + private const val ATTR_VALUE = "value" + private const val ATTR_ORIGIN = "origin" + + private const val ORIGIN = "Datadog official GitHub release" + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/verification/VerificationXmlPlugin.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/verification/VerificationXmlPlugin.kt new file mode 100644 index 0000000000..7412184d0d --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/plugin/verification/VerificationXmlPlugin.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.verification + +import com.android.build.gradle.internal.tasks.factory.dependsOn +import org.gradle.api.Plugin +import org.gradle.api.Project + +class VerificationXmlPlugin : Plugin { + + override fun apply(target: Project) { + val genTask = target.tasks.register(TASK_GEN_VERIFICATION_XML, GenerateVerificationXmlTask::class.java) + + target.afterEvaluate { + genTask.dependsOn("bundleReleaseAar") + genTask.dependsOn("javaDocReleaseJar") + genTask.dependsOn("sourceReleaseJar") + genTask.dependsOn("generatePomFileForReleasePublication") + genTask.dependsOn("generateMetadataFileForReleasePublication") + genTask.dependsOn("signReleasePublication") + + getTasksByName("publishToSonatype", false).forEach { + it.dependsOn(genTask) + } + getTasksByName("publishToMavenLocal", false).forEach { + it.dependsOn(genTask) + } + } + } + + companion object { + + const val TASK_GEN_VERIFICATION_XML = "generateVerificationXml" + const val XML_FILE_NAME = "verification-metadata.xml" + } +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/utils/NodeListSequence.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/NodeListSequence.kt index f200558440..c8197302ac 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/utils/NodeListSequence.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/NodeListSequence.kt @@ -6,9 +6,9 @@ package com.datadog.gradle.utils -import kotlin.collections.Iterator as KIterator import org.w3c.dom.Node import org.w3c.dom.NodeList +import kotlin.collections.Iterator as KIterator class NodeListSequence( private val nodeList: NodeList @@ -22,8 +22,12 @@ class NodeListSequence( private val nodeList: NodeList ) : KIterator { private var i = 0 - override fun hasNext() = nodeList.length > i - override fun next(): Node = nodeList.item(i++) + override fun hasNext() = i < nodeList.length + override fun next(): Node = if (i < nodeList.length) { + nodeList.item(i++) + } else { + throw NoSuchElementException("There's no next element!") + } } } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/utils/StringExt.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/StringExt.kt new file mode 100644 index 0000000000..9449e157b4 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/StringExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.utils + +import java.util.Locale + +fun List.joinToCamelCase(): String = when (size) { + 0 -> throw IllegalArgumentException("invalid section size, cannot be zero") + 1 -> this[0].toCamelCase() + else -> this.joinToString("", transform = String::toCamelCase) +} + +fun List.joinToCamelCaseAsVar(): String = when (size) { + 0 -> throw IllegalArgumentException("invalid section size, cannot be zero") + 1 -> this[0].toCamelCaseAsVar() + else -> get(0).toCamelCaseAsVar() + drop(1).joinToCamelCase() +} + +@Suppress("ReturnCount") +fun String.toCamelCase(): String { + val split = this.split("_") + if (split.size == 0) return "" + if (split.size == 1) { + return split[0].replaceFirstChar { + if (it.isLowerCase()) { + it.titlecase( + Locale.US + ) + } else { + it.toString() + } + } + } + return split.joinToCamelCase() +} + +@Suppress("ReturnCount") +fun String.toCamelCaseAsVar(): String { + val split = this.split("_") + if (split.isEmpty()) return "" + if (split.size == 1) return split[0] + return split.joinToCamelCaseAsVar() +} diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/utils/SystemUtils.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/SystemUtils.kt index 64458f134a..3667789b94 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/utils/SystemUtils.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/SystemUtils.kt @@ -6,14 +6,14 @@ package com.datadog.gradle.utils +import org.gradle.process.ExecOperations import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStreamReader -import org.gradle.api.Project -fun Project.execShell(vararg command: String): List { +fun ExecOperations.execShell(vararg command: String): List { val outputStream = ByteArrayOutputStream() - this.exec { + exec { commandLine(*command) standardOutput = outputStream } diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/utils/Version.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/Version.kt index 38966c8522..f5d1f4770c 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/utils/Version.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/utils/Version.kt @@ -34,6 +34,14 @@ data class Version( data class Alpha(val number: Int) : Type() { override val suffix: String = "-alpha$number" } + + object Dev : Type() { + override val suffix: String = "-dev" + } + + object Snapshot : Type() { + override val suffix: String = "-SNAPSHOT" + } } // endregion diff --git a/buildSrc/src/test/kotlin/com/datadog/android/core/internal/utils/JsonSerializer.kt b/buildSrc/src/test/kotlin/com/datadog/android/core/internal/utils/JsonSerializer.kt new file mode 100644 index 0000000000..5d89b900f0 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/datadog/android/core/internal/utils/JsonSerializer.kt @@ -0,0 +1,44 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import org.json.JSONArray +import org.json.JSONObject +import java.util.Date + +object JsonSerializer { + val NULL_MAP_VALUE: Object = Object() + + fun toJsonElement(item: Any?): JsonElement { + return when (item) { + NULL_MAP_VALUE -> JsonNull.INSTANCE + null -> JsonNull.INSTANCE + JsonNull.INSTANCE -> JsonNull.INSTANCE + is Boolean -> JsonPrimitive(item) + is Int -> JsonPrimitive(item) + is Long -> JsonPrimitive(item) + is Float -> JsonPrimitive(item) + is Double -> JsonPrimitive(item) + is String -> JsonPrimitive(item) + is Date -> JsonPrimitive(item.time) + // this line should come before Iterable, otherwise this branch is never executed + is JsonArray -> item + is Iterable<*> -> item.toJsonArray() + is Map<*, *> -> item.toJsonElement() + is JsonObject -> item + is JsonPrimitive -> item + is JSONObject -> item.toJsonElement() + is JSONArray -> item.toJsonArray() + else -> JsonPrimitive(item.toString()) + } + } +} diff --git a/buildSrc/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt b/buildSrc/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt new file mode 100644 index 0000000000..95c6d09a16 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt @@ -0,0 +1,84 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import java.util.Date + +// Placeholder functions needed for Deserializer code generator. These functions are needed for +// unit tests and will be overridden in the main project. + +internal fun Any?.toJsonElement(): JsonElement { + return when (this) { + null -> JsonNull.INSTANCE + JsonNull.INSTANCE -> JsonNull.INSTANCE + is Boolean -> JsonPrimitive(this) + is Int -> JsonPrimitive(this) + is Long -> JsonPrimitive(this) + is Float -> JsonPrimitive(this) + is Double -> JsonPrimitive(this) + is String -> JsonPrimitive(this) + is Date -> JsonPrimitive(this.time) + is Iterable<*> -> this.toJsonArray() + is JsonObject -> this + is JsonArray -> this + is JsonPrimitive -> this + is Map<*, *> -> JsonObject().apply { + forEach { (k, v) -> + add(k.toString(), v.toJsonElement()) + } + } + else -> JsonPrimitive(toString()) + } +} + +internal fun Any?.fromJsonElement(): Any? { + return when (this) { + is JsonNull -> null + is JsonPrimitive -> { + if (this.isBoolean) { + this.asBoolean + } else if (this.isNumber) { + this.asNumber + } else if (this.isString) { + this.asString + } else { + this + } + } + is JsonObject -> this.asDeepMap() + else -> this + } +} + +internal fun Iterable<*>.toJsonArray(): JsonElement { + val array = JsonArray() + forEach { + array.add(it.toJsonElement()) + } + return array +} + +internal fun JsonObject.asDeepMap(): Map { + val map = mutableMapOf() + entrySet().forEach { + map[it.key] = it.value.fromJsonElement() + } + return map +} + +internal fun JsonElement?.asDeepMap(): Map { + return if (this is JsonObject) { + this.asDeepMap() + } else { + emptyMap() + } +} diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitorTest.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitorTest.kt index b9115e6196..453ea7c609 100644 --- a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitorTest.kt +++ b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/apisurface/KotlinFileVisitorTest.kt @@ -6,13 +6,13 @@ package com.datadog.gradle.plugin.apisurface -import java.io.File -import java.lang.IllegalStateException -import junit.framework.Assert.assertEquals +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import java.io.File +import java.lang.IllegalStateException internal class KotlinFileVisitorTest { @@ -35,7 +35,7 @@ internal class KotlinFileVisitorTest { package foo.bar class Spam { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -52,7 +52,7 @@ internal class KotlinFileVisitorTest { """ class Spam { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -70,7 +70,7 @@ internal class KotlinFileVisitorTest { package foo.bar object Spam { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -88,7 +88,7 @@ internal class KotlinFileVisitorTest { package foo.bar interface Spam { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -107,7 +107,7 @@ internal class KotlinFileVisitorTest { enum class Spam { A, B, C; } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -121,6 +121,28 @@ internal class KotlinFileVisitorTest { ) } + @Test + fun `describes public annotation`() { + tempFile.writeText( + """ + package foo.bar + annotation class Spam( + val A: String, + val B: String, + val C: String + ) + """.trimIndent() + ) + + testedVisitor.visitFile(tempFile) + + assertEquals( + "annotation foo.bar.Spam\n" + + " constructor(String, String, String)\n", + testedVisitor.description.toString() + ) + } + @Test fun `describes public sealed class`() { tempFile.writeText( @@ -131,7 +153,7 @@ internal class KotlinFileVisitorTest { class B : Spam() class C : Spam() } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -154,7 +176,7 @@ internal class KotlinFileVisitorTest { internal class C {} private class D {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -173,7 +195,7 @@ internal class KotlinFileVisitorTest { class Spam { init {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -193,7 +215,7 @@ internal class KotlinFileVisitorTest { internal object C {} private object D {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -213,7 +235,7 @@ internal class KotlinFileVisitorTest { internal interface C {} private interface D {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -234,7 +256,7 @@ internal class KotlinFileVisitorTest { internal fun youCantSeeThis() {} private fun youCantSeeThisEither() {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -256,7 +278,7 @@ internal class KotlinFileVisitorTest { internal fun youCantSeeThis() private fun youCantSeeThisEither() } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -278,7 +300,7 @@ internal class KotlinFileVisitorTest { internal val youCantSeeThis = 0 private val youCantSeeThisEither = false } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -298,7 +320,7 @@ internal class KotlinFileVisitorTest { class Spam(i : Int) { private constructor(s : String) : this(s.length) } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -319,7 +341,7 @@ internal class KotlinFileVisitorTest { internal constructor(i : Int) { constructor(s : String) : this(s.length) } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -340,7 +362,7 @@ internal class KotlinFileVisitorTest { open class C {} abstract class D {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -362,7 +384,7 @@ internal class KotlinFileVisitorTest { open fun doSomething() : String = "" abstract fun doSomethingElse() {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -384,7 +406,7 @@ internal class KotlinFileVisitorTest { protected open fun doSomething() {} protected abstract fun doSomethingElse() {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -406,7 +428,7 @@ internal class KotlinFileVisitorTest { open val s:String = "" abstract var i : Int = 0 } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -427,7 +449,7 @@ internal class KotlinFileVisitorTest { import java.io.IOException class Spam { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -453,7 +475,7 @@ internal class KotlinFileVisitorTest { TODO() } } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -466,6 +488,29 @@ internal class KotlinFileVisitorTest { ) } + @Test + fun `describes wildcard generics in functions`() { + tempFile.writeText( + """ + package foo.bar + import java.io.IOException + class Spam { + fun doSomethingElse(map : Map<*, *>) : Pair, List<*>> { + TODO() + } + } + """.trimIndent() + ) + + testedVisitor.visitFile(tempFile) + + assertEquals( + "class foo.bar.Spam\n" + + " fun doSomethingElse(Map<*, *>): Pair, List<*>>\n", + testedVisitor.description.toString() + ) + } + @Test fun `describes parent class and interfaces in class types`() { tempFile.writeText( @@ -476,7 +521,7 @@ internal class KotlinFileVisitorTest { import java.util.Comparator class Spam:IOException, Runnable, Comparator { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -494,7 +539,7 @@ internal class KotlinFileVisitorTest { package foo.bar class Spam : Bar.AbstractCallback(), Baz.Listener { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -513,7 +558,7 @@ internal class KotlinFileVisitorTest { class Spam { fun doSomething(i: Int, s: String?, l : List) {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -532,7 +577,7 @@ internal class KotlinFileVisitorTest { package foo.bar class Spam (i: Int, s: String? = null, l : List = emptyList()) { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -552,7 +597,7 @@ internal class KotlinFileVisitorTest { class Spam { fun doSomething(i: Int, s: String? = null, l : List = emptyList()) {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -572,7 +617,7 @@ internal class KotlinFileVisitorTest { class Spam : Runnable{ override fun run ( ) { } } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -594,8 +639,9 @@ internal class KotlinFileVisitorTest { fun unit(block : (String) -> Unit) {} fun nullable(block : (String) -> Any?) {} fun withReceiver(block : String.() -> Int) {} + fun withNamedArgumentOfLambda(block : (name: String) -> Unit) {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -605,7 +651,8 @@ internal class KotlinFileVisitorTest { " fun withInputs((String, Char) -> Int)\n" + " fun unit((String) -> Unit)\n" + " fun nullable((String) -> Any?)\n" + - " fun withReceiver(String.() -> Int)\n", + " fun withReceiver(String.() -> Int)\n" + + " fun withNamedArgumentOfLambda((String) -> Unit)\n", testedVisitor.description.toString() ) } @@ -617,7 +664,7 @@ internal class KotlinFileVisitorTest { package foo.bar class Spam (block : (String, Char) -> Int) { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -637,7 +684,7 @@ internal class KotlinFileVisitorTest { object Global { const val DATA : String= "Something" } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -659,7 +706,7 @@ internal class KotlinFileVisitorTest { const val DATA : String= "Something" } } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -682,7 +729,7 @@ internal class KotlinFileVisitorTest { const val DATA = "Something" } } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -697,7 +744,7 @@ internal class KotlinFileVisitorTest { class Spam { fun doSomething() : FooData } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -719,7 +766,7 @@ internal class KotlinFileVisitorTest { fun doSomething() : Data fun doSomethingElse() : not.same.Data } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -733,12 +780,12 @@ internal class KotlinFileVisitorTest { } @Test - fun `describes type alias`() { + fun `describes function type alias`() { tempFile.writeText( """ typealias StringTransform = (String) -> String? typealias StringRepeat = (String, Int) -> String - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -750,13 +797,29 @@ internal class KotlinFileVisitorTest { ) } + @Test + fun `describes class type alias`() { + tempFile.writeText( + """ + typealias PowerfulString = String + """.trimIndent() + ) + + testedVisitor.visitFile(tempFile) + + assertEquals( + "typealias PowerfulString = String\n", + testedVisitor.description.toString() + ) + } + @Test fun `ignores non public type alias`() { tempFile.writeText( """ internal typealias StringTransform = (String) -> String? private typealias IntTransform = (Int) -> Int - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -775,7 +838,7 @@ internal class KotlinFileVisitorTest { @Deprecated class Spam { } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -797,7 +860,7 @@ internal class KotlinFileVisitorTest { @Deprecated fun doSomething() {} } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -820,7 +883,7 @@ internal class KotlinFileVisitorTest { @Deprecated constructor(s : String) : this(s.length) } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -844,7 +907,7 @@ internal class KotlinFileVisitorTest { @Deprecated val foo : String = "" } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -867,7 +930,7 @@ internal class KotlinFileVisitorTest { @Deprecated("Don't use anymore") const val foo : String = "" } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -889,7 +952,7 @@ internal class KotlinFileVisitorTest { class Spam { val data : String = "" } - """.trimIndent() + """.trimIndent() ) testedVisitor.visitFile(tempFile) @@ -901,6 +964,22 @@ internal class KotlinFileVisitorTest { ) } + @Test + fun `describes extension functions`() { + tempFile.writeText( + """ + fun String.withFooBar(): String = this + "foobar" + """.trimIndent() + ) + + testedVisitor.visitFile(tempFile) + + assertEquals( + "fun String.withFooBar(): String\n", + testedVisitor.description.toString() + ) + } + companion object { const val FILE_NAME = "file.kt" } diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReaderTest.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReaderTest.kt index 15e23a8c1a..36c27be751 100644 --- a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReaderTest.kt +++ b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/JsonSchemaReaderTest.kt @@ -6,13 +6,13 @@ package com.datadog.gradle.plugin.jsonschema -import java.io.File import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.Parameterized +import java.io.File @RunWith(Parameterized::class) class JsonSchemaReaderTest( @@ -30,7 +30,14 @@ class JsonSchemaReaderTest( tempDir = tempFolderRule.newFolder() val clazz = JsonSchemaReaderTest::class.java val inputPath = clazz.getResource("/input/$inputSchema.json").file - val testedReader = JsonSchemaReader(mapOf("all_of_merged.json" to "UserMerged")) + val testedReader = JsonSchemaReader( + mapOf( + "all_of_merged.json" to "UserMerged", + "additional_props_merged.json" to "AdditionalPropsMerged", + "additional_props_single_merge.json" to "AdditionalPropsSingleMerge" + ), + NoOpLogger() + ) val generatedType = testedReader.readSchema(File(inputPath)) @@ -46,25 +53,40 @@ class JsonSchemaReaderTest( @Parameterized.Parameters(name = "{index}: {0}") fun data(): Collection> { return listOf( - arrayOf("minimal", Person), - arrayOf("required", Product), - arrayOf("nested", Book), arrayOf("arrays", Article), - arrayOf("sets", Video), + arrayOf("one_of", Animal), + arrayOf("defaults_with_optionals", Bike), + arrayOf("nested", Book), + arrayOf("additional_props", Comment), + arrayOf("additional_props_any", Company), + arrayOf("additional_props_merged", AdditionalPropsMerged), + arrayOf("additional_props_single_merge", AdditionalPropsSingleMerge), + arrayOf("definition_name_conflict", Conflict), + arrayOf("root_schema_with_no_type", Country), arrayOf("definition", Customer), arrayOf("definition_with_id", Customer), - arrayOf("enum", Style), - arrayOf("constant", Location), - arrayOf("constant_number", Version), arrayOf("nested_enum", DateTime), - arrayOf("description", Opus), - arrayOf("top_level_definition", Foo), + arrayOf("external_description", Delivery), arrayOf("types", Demo), + arrayOf("external_description_complex_path", Employee), + arrayOf("top_level_definition", Foo), + arrayOf("one_of_ref", Household), + arrayOf("enum_number", Jacket), + arrayOf("constant", Location), + arrayOf("read_only", Message), + arrayOf("enum_array", Order), + arrayOf("description", Opus), + arrayOf("one_of_complex", Paper), + arrayOf("minimal", Person), + arrayOf("required", Product), + arrayOf("external_nested_description", Shipping), + arrayOf("external_nested_description_properties", Shipping), + arrayOf("enum", Style), arrayOf("all_of", User), arrayOf("all_of_merged", UserMerged), - arrayOf("external_description", Delivery), - arrayOf("external_nested_description", Shipping), - arrayOf("definition_name_conflict", Conflict) + arrayOf("constant_number", Version), + arrayOf("sets", Video), + arrayOf("one_of_nested", WeirdCombo) ) } } diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/ModelValidationTest.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/ModelValidationTest.kt index 2a602acfb2..0fc220c360 100644 --- a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/ModelValidationTest.kt +++ b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/ModelValidationTest.kt @@ -6,8 +6,15 @@ package com.datadog.gradle.plugin.jsonschema +import com.datadog.android.core.internal.utils.fromJsonElement import com.example.forgery.ForgeryConfiguration +import com.example.model.Company +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject import fr.xgouchet.elmyr.junit4.ForgeRule +import org.assertj.core.api.Assertions.assertThat import org.everit.json.schema.loader.SchemaLoader import org.json.JSONObject import org.json.JSONTokener @@ -16,11 +23,12 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import java.util.Date @RunWith(Parameterized::class) class ModelValidationTest( internal val schemaResourcePath: String, - internal val className: String + internal val outputInfo: OutputInfo ) { @get:Rule @@ -33,11 +41,10 @@ class ModelValidationTest( @Test fun `validates model`() { - val type = Class.forName("com.example.model.$className") - val toJson = type.getDeclaredMethod("toJson") + val type = Class.forName("com.example.model.${outputInfo.className}") + val toJson = type.getMethod("toJson") val schema = loadSchema(schemaResourcePath) val file = javaClass.getResource("/input/").file - println(">> SCOPE PATH : file://$file") val schemaLoader = SchemaLoader.builder() .resolutionScope("file://$file") .schemaJson(schema) @@ -51,42 +58,159 @@ class ModelValidationTest( validator.validate(JSONObject(json.toString())) } catch (e: Exception) { throw RuntimeException( - "Failed to validate $schemaResourcePath:\n$entity\n$json\n", e + "Failed to validate $schemaResourcePath (#$it):\n$entity\n$json\n", + e ) } } } + @Test + fun `validate model serialization and deserialization`() { + val type = Class.forName("com.example.model.${outputInfo.className}") + val toJson = type.getMethod("toJson") + if (outputInfo.isConstant) { + // skip this test as is not relevant anymore. We are just testing a constructor. + return + } + val generatorFunction = type.getMethod("fromJson", String::class.java) + repeat(10) { + val entity = forge.getForgery(type) + val json = toJson.invoke(entity).toString() + val generatedModel = generatorFunction.invoke(null, json) + + assertThat(generatedModel) + .overridingErrorMessage( + "Deserialized model was not the same " + + "with the serialized for type: [$type] and test iteration: [$it]\n" + + " - input: $entity \n" + + " - json: $json \n" + + " - output: $generatedModel" + + ) + .usingRecursiveComparison() + .withComparatorForType(numberTypeComparator, Number::class.java) + .withComparatorForType(mapTypeComparator, Map::class.java) + .withComparatorForType(informationComparator, Company.Information::class.java) + .ignoringCollectionOrder() + .isEqualTo(entity) + } + } + + private val numberTypeComparator = Comparator { t1, t2 -> + when (t2) { + is Long -> t2.compareTo(t1.toLong()) + is Double -> t2.compareTo(t1.toDouble()) + is Float -> t2.compareTo(t1.toFloat()) + is Byte -> t2.compareTo(t1.toByte()) + else -> (t2 as Int).compareTo(t1.toInt()) + } + } + + private val mapTypeComparator = Comparator> { t1, t2 -> + if (t2.size == t1.size) { + val mismatches = t1.filter { (k, v1) -> + val v2 = t2[k] + compareMapValues(v1, v2) + } + mismatches.size + } else { + 1 + } + } + + private val informationComparator = Comparator { t1, t2 -> + if (t1.date != t2.date) { + -1 + } else if (t1.priority != t2.priority) { + -2 + } else { + mapTypeComparator.compare(t1.additionalProperties, t2.additionalProperties) + } + } + + private fun compareMapValues(v1: Any?, v2: Any?): Boolean { + return if (v1 is JsonElement) { + compareJsonElement(v1, v2) + } else if (v1 is Map<*, *> && v2 is Map<*, *>) { + mapTypeComparator.compare(v1, v2) == 0 + } else { + v2 != v1 + } + } + + private fun compareJsonElement( + v1: JsonElement, + v2: Any? + ): Boolean { + return when (v2) { + null -> v1 != JsonNull.INSTANCE + is Boolean -> v1.asBoolean != v2 + is Int -> v1.asInt != v2 + is Long -> v1.asLong != v2 + is Float -> v1.asFloat != v2 + is Double -> v1.asDouble != v2 + is String -> v1.asString != v2 + is Date -> v1.asLong != v2.time + is JsonObject -> v1.asJsonObject.toString() != v2.toString() + is JsonArray -> v1.asJsonArray != v2 + is Iterable<*> -> v1.asJsonArray.toList() != v2 + is Map<*, *> -> mapTypeComparator.compare(v1.asJsonObject.asDeepMap(), v2) == 0 + else -> v1.asString != v2.toString() + } + } + private fun loadSchema(schemaResName: String): JSONObject { return javaClass.getResourceAsStream("/input/$schemaResName.json").use { JSONObject(JSONTokener(it)) } } + internal fun JsonObject.asDeepMap(): Map { + val map = mutableMapOf() + entrySet().forEach { + map[it.key] = it.value.fromJsonElement() + } + return map + } + companion object { + @JvmStatic @Parameterized.Parameters(name = "{index}: {1}") fun data(): Collection> { return listOf( - arrayOf("minimal", "Person"), - arrayOf("required", "Product"), - arrayOf("nested", "Book"), - arrayOf("arrays", "Article"), - arrayOf("sets", "Video"), - arrayOf("definition", "Customer"), - arrayOf("definition_with_id", "Customer"), - arrayOf("enum", "Style"), - arrayOf("constant", "Location"), - arrayOf("constant_number", "Version"), - arrayOf("nested_enum", "DateTime"), - arrayOf("description", "Opus"), - arrayOf("top_level_definition", "Foo"), - arrayOf("types", "Demo"), - arrayOf("all_of", "User"), - arrayOf("external_description", "Delivery"), - arrayOf("external_nested_description", "Shipping"), - arrayOf("definition_name_conflict", "Conflict") + arrayOf("arrays", OutputInfo("Article")), + arrayOf("one_of", OutputInfo("Animal")), + arrayOf("defaults_with_optionals", OutputInfo("Bike")), + arrayOf("nested", OutputInfo("Book")), + arrayOf("additional_props", OutputInfo("Comment")), + arrayOf("additional_props_any", OutputInfo("Company")), + arrayOf("definition_name_conflict", OutputInfo("Conflict")), + arrayOf("definition", OutputInfo("Customer")), + arrayOf("definition_with_id", OutputInfo("Customer")), + arrayOf("nested_enum", OutputInfo("DateTime")), + arrayOf("external_description", OutputInfo("Delivery")), + arrayOf("types", OutputInfo("Demo")), + arrayOf("top_level_definition", OutputInfo("Foo")), + arrayOf("one_of_ref", OutputInfo("Household")), + arrayOf("enum_number", OutputInfo("Jacket")), + arrayOf("constant", OutputInfo("Location", true)), + arrayOf("read_only", OutputInfo("Message")), + arrayOf("enum_array", OutputInfo("Order")), + arrayOf("description", OutputInfo("Opus")), + arrayOf("minimal", OutputInfo("Person")), + arrayOf("required", OutputInfo("Product")), + arrayOf("external_nested_description", OutputInfo("Shipping")), + arrayOf("enum", OutputInfo("Style")), + arrayOf("all_of", OutputInfo("User")), + arrayOf("all_of_merged", OutputInfo("UserMerged")), + arrayOf("constant_number", OutputInfo("Version")), + arrayOf("sets", OutputInfo("Video")), + arrayOf("one_of_nested", OutputInfo("WeirdCombo")) ) } } + + data class OutputInfo(val className: String, val isConstant: Boolean = false) } diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/NoOpLogger.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/NoOpLogger.kt new file mode 100644 index 0000000000..7cabde0141 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/NoOpLogger.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema + +import org.gradle.api.logging.LogLevel +import org.gradle.api.logging.Logger +import org.slf4j.Marker + +@Suppress("MethodOverloading") +class NoOpLogger : Logger { + override fun getName(): String = "NoOp" + + override fun isTraceEnabled(): Boolean = false + + override fun isTraceEnabled(marker: Marker?): Boolean = false + + override fun trace(msg: String?) {} + + override fun trace(format: String?, arg: Any?) {} + + override fun trace(format: String?, arg1: Any?, arg2: Any?) {} + + override fun trace(format: String?, vararg arguments: Any?) {} + + override fun trace(msg: String?, t: Throwable?) {} + + override fun trace(marker: Marker?, msg: String?) {} + + override fun trace(marker: Marker?, format: String?, arg: Any?) {} + + override fun trace(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) {} + + override fun trace(marker: Marker?, format: String?, vararg argArray: Any?) {} + + override fun trace(marker: Marker?, msg: String?, t: Throwable?) {} + + override fun isDebugEnabled(): Boolean = false + + override fun isDebugEnabled(marker: Marker?): Boolean = false + + override fun debug(message: String?, vararg objects: Any?) {} + + override fun debug(msg: String?) {} + + override fun debug(format: String?, arg: Any?) {} + + override fun debug(format: String?, arg1: Any?, arg2: Any?) {} + + override fun debug(msg: String?, t: Throwable?) {} + + override fun debug(marker: Marker?, msg: String?) {} + + override fun debug(marker: Marker?, format: String?, arg: Any?) {} + + override fun debug(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) {} + + override fun debug(marker: Marker?, format: String?, vararg arguments: Any?) {} + + override fun debug(marker: Marker?, msg: String?, t: Throwable?) {} + + override fun isInfoEnabled(): Boolean = false + + override fun isInfoEnabled(marker: Marker?): Boolean = false + + override fun info(message: String?, vararg objects: Any?) {} + + override fun info(msg: String?) {} + + override fun info(format: String?, arg: Any?) {} + + override fun info(format: String?, arg1: Any?, arg2: Any?) {} + + override fun info(msg: String?, t: Throwable?) {} + + override fun info(marker: Marker?, msg: String?) {} + + override fun info(marker: Marker?, format: String?, arg: Any?) {} + + override fun info(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) {} + + override fun info(marker: Marker?, format: String?, vararg arguments: Any?) {} + + override fun info(marker: Marker?, msg: String?, t: Throwable?) {} + + override fun isWarnEnabled(): Boolean = false + + override fun isWarnEnabled(marker: Marker?): Boolean = false + + override fun warn(msg: String?) {} + + override fun warn(format: String?, arg: Any?) {} + + override fun warn(format: String?, vararg arguments: Any?) {} + + override fun warn(format: String?, arg1: Any?, arg2: Any?) {} + + override fun warn(msg: String?, t: Throwable?) {} + + override fun warn(marker: Marker?, msg: String?) {} + + override fun warn(marker: Marker?, format: String?, arg: Any?) {} + + override fun warn(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) {} + + override fun warn(marker: Marker?, format: String?, vararg arguments: Any?) {} + + override fun warn(marker: Marker?, msg: String?, t: Throwable?) {} + + override fun isErrorEnabled(): Boolean = false + + override fun isErrorEnabled(marker: Marker?): Boolean = false + + override fun error(msg: String?) {} + + override fun error(format: String?, arg: Any?) {} + + override fun error(format: String?, arg1: Any?, arg2: Any?) {} + + override fun error(format: String?, vararg arguments: Any?) {} + + override fun error(msg: String?, t: Throwable?) {} + + override fun error(marker: Marker?, msg: String?) {} + + override fun error(marker: Marker?, format: String?, arg: Any?) {} + + override fun error(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) {} + + override fun error(marker: Marker?, format: String?, vararg arguments: Any?) {} + + override fun error(marker: Marker?, msg: String?, t: Throwable?) {} + + override fun isLifecycleEnabled(): Boolean = false + + override fun lifecycle(message: String?) {} + + override fun lifecycle(message: String?, vararg objects: Any?) {} + + override fun lifecycle(message: String?, throwable: Throwable?) {} + + override fun isQuietEnabled(): Boolean = false + + override fun quiet(message: String?) {} + + override fun quiet(message: String?, vararg objects: Any?) {} + + override fun quiet(message: String?, throwable: Throwable?) {} + + override fun isEnabled(level: LogLevel?): Boolean = false + + override fun log(level: LogLevel?, message: String?) {} + + override fun log(level: LogLevel?, message: String?, vararg objects: Any?) {} + + override fun log(level: LogLevel?, message: String?, throwable: Throwable?) {} +} diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/PokoGeneratorTest.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/PokoGeneratorTest.kt deleted file mode 100644 index b14c70387b..0000000000 --- a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/PokoGeneratorTest.kt +++ /dev/null @@ -1,85 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.gradle.plugin.jsonschema - -import java.io.File -import java.nio.file.Files -import java.nio.file.Paths -import org.assertj.core.api.Assertions.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -class PokoGeneratorTest( - internal val inputType: TypeDefinition, - internal val outputFile: String -) { - - @get:Rule - val tempFolderRule = TemporaryFolder() - - lateinit var tempDir: File - - @Test - fun `generates a Poko file`() { - tempDir = tempFolderRule.newFolder() - val clazz = PokoGeneratorTest::class.java - val outputPath = clazz.getResource("/output/$outputFile.kt").file - val testedGenerator = PokoGenerator(tempDir, "com.example.model") - - testedGenerator.generate(inputType) - - val generatedFile = Files.find( - Paths.get(tempDir.toURI()), - Integer.MAX_VALUE, - { _, attrs -> attrs.isRegularFile } - ).findFirst().get().toFile() - - val generatedContent = generatedFile.readText(Charsets.UTF_8) - val outputContent = File(outputPath).readText(Charsets.UTF_8) - assertThat(generatedContent) - .overridingErrorMessage( - "File $outputFile generated from type \n$inputType \ndidn't match expectation:\n" + - "<<<<<<< EXPECTED\n" + - outputContent + - "=======\n" + - generatedContent + - "\n>>>>>>> GENERATED\n" - ) - .isEqualTo(outputContent) - } - - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{index}: {1}") - fun data(): Collection> { - return listOf( - arrayOf(Article, "Article"), - arrayOf(Book, "Book"), - arrayOf(Conflict, "Conflict"), - arrayOf(Customer, "Customer"), - arrayOf(DateTime, "DateTime"), - arrayOf(Delivery, "Delivery"), - arrayOf(Demo, "Demo"), - arrayOf(Foo, "Foo"), - arrayOf(Person, "Person"), - arrayOf(Location, "Location"), - arrayOf(Opus, "Opus"), - arrayOf(Product, "Product"), - arrayOf(Shipping, "Shipping"), - arrayOf(Style, "Style"), - arrayOf(Version, "Version"), - arrayOf(Video, "Video"), - arrayOf(User, "User"), - arrayOf(UserMerged, "UserMerged") - ) - } - } -} diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/TestDefinitions.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/TestDefinitions.kt index 02e1386653..c66b20f464 100644 --- a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/TestDefinitions.kt +++ b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/TestDefinitions.kt @@ -9,24 +9,81 @@ package com.datadog.gradle.plugin.jsonschema val Address = TypeDefinition.Class( name = "Address", properties = listOf( - TypeProperty("street_address", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("city", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("state", TypeDefinition.Primitive(JsonType.STRING), false) + TypeProperty( + name = "street_address", + type = TypeDefinition.Primitive(JsonPrimitiveType.STRING), + optional = false + ), + TypeProperty( + name = "city", + type = TypeDefinition.Primitive(JsonPrimitiveType.STRING), + optional = false + ), + TypeProperty( + name = "state", + type = TypeDefinition.Primitive(JsonPrimitiveType.STRING), + optional = false + ) ) ) +val Animal = TypeDefinition.OneOfClass( + name = "Animal", + options = listOf( + TypeDefinition.Class( + name = "Fish", + properties = listOf( + TypeProperty( + name = "water", + type = TypeDefinition.Enum("Water", JsonType.STRING, listOf("salt", "fresh")), + optional = false + ), + TypeProperty( + name = "size", + type = TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + optional = true + ) + ) + ), + TypeDefinition.Class( + name = "Bird", + properties = listOf( + TypeProperty( + name = "food", + type = TypeDefinition.Enum( + name = "Food", + type = JsonType.STRING, + values = listOf( + "fish", + "bird", + "rodent", + "insect", + "fruit", + "seeds", + "pollen" + ) + ), + optional = false + ), + TypeProperty("can_fly", TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), false) + ) + ) + ), + description = "A representation of the animal kingdom" +) + val Article = TypeDefinition.Class( name = "Article", properties = listOf( - TypeProperty("title", TypeDefinition.Primitive(JsonType.STRING), false), + TypeProperty("title", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), TypeProperty( "tags", - TypeDefinition.Array(TypeDefinition.Primitive(JsonType.STRING)), + TypeDefinition.Array(TypeDefinition.Primitive(JsonPrimitiveType.STRING)), true ), TypeProperty( "authors", - TypeDefinition.Array(TypeDefinition.Primitive(JsonType.STRING)), + TypeDefinition.Array(TypeDefinition.Primitive(JsonPrimitiveType.STRING)), false ) ) @@ -35,15 +92,24 @@ val Article = TypeDefinition.Class( val Book = TypeDefinition.Class( name = "Book", properties = listOf( - TypeProperty("bookId", TypeDefinition.Primitive(JsonType.INTEGER), false), - TypeProperty("title", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("price", TypeDefinition.Primitive(JsonType.NUMBER), false), + TypeProperty("bookId", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), false), + TypeProperty("title", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), + TypeProperty("price", TypeDefinition.Primitive(JsonPrimitiveType.NUMBER), false), TypeProperty( - "author", TypeDefinition.Class( + "author", + TypeDefinition.Class( name = "Author", properties = listOf( - TypeProperty("firstName", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("lastName", TypeDefinition.Primitive(JsonType.STRING), false), + TypeProperty( + "firstName", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + false + ), + TypeProperty( + "lastName", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + false + ), TypeProperty( "contact", TypeDefinition.Class( @@ -51,12 +117,12 @@ val Book = TypeDefinition.Class( properties = listOf( TypeProperty( "phone", - TypeDefinition.Primitive(JsonType.STRING), + TypeDefinition.Primitive(JsonPrimitiveType.STRING), true ), TypeProperty( "email", - TypeDefinition.Primitive(JsonType.STRING), + TypeDefinition.Primitive(JsonPrimitiveType.STRING), true ) ) @@ -73,21 +139,134 @@ val Book = TypeDefinition.Class( val Customer = TypeDefinition.Class( name = "Customer", properties = listOf( - TypeProperty("name", TypeDefinition.Primitive(JsonType.STRING), true), + TypeProperty("name", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), TypeProperty("billing_address", Address, true), TypeProperty("shipping_address", Address, true) ) ) +val Comment = TypeDefinition.Class( + name = "Comment", + properties = listOf( + TypeProperty("message", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty( + "ratings", + TypeDefinition.Class( + name = "Ratings", + properties = listOf( + TypeProperty( + "global", + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + false + ) + ), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + optional = true, + readOnly = true + ) + ), + true + ), + TypeProperty( + "flags", + TypeDefinition.Class( + name = "Flags", + properties = listOf(), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), + optional = true, + readOnly = false + ) + ), + true + ), + TypeProperty( + "tags", + TypeDefinition.Class( + name = "Tags", + properties = listOf(), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Primitive(JsonPrimitiveType.STRING), + optional = true, + readOnly = false + ) + ), + true + ) + ) +) + +val Company = TypeDefinition.Class( + name = "Company", + properties = listOf( + TypeProperty("name", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty( + "ratings", + TypeDefinition.Class( + name = "Ratings", + properties = listOf( + TypeProperty( + "global", + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + false + ) + ), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + optional = true, + readOnly = false + ) + ), + true + ), + TypeProperty( + "information", + TypeDefinition.Class( + name = "Information", + properties = listOf( + TypeProperty( + "date", + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + true + ), + TypeProperty( + "priority", + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + true + ) + ), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Class("?", emptyList()), + optional = true, + readOnly = false + ) + ), + true + ) + ), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Class("?", emptyList()), + optional = true, + readOnly = false + ) +) + val Conflict = TypeDefinition.Class( name = "Conflict", properties = listOf( TypeProperty( "type", TypeDefinition.Class( - name = "Type", + name = "ConflictType", properties = listOf( - TypeProperty("id", TypeDefinition.Primitive(JsonType.STRING), true) + TypeProperty("id", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true) ) ), true @@ -97,11 +276,11 @@ val Conflict = TypeDefinition.Class( TypeDefinition.Class( name = "User", properties = listOf( - TypeProperty("name", TypeDefinition.Primitive(JsonType.STRING), true), + TypeProperty("name", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), TypeProperty( "type", TypeDefinition.Enum( - name = "Type", + name = "UserType", type = JsonType.STRING, values = listOf("unknown", "customer", "partner") ), @@ -122,18 +301,20 @@ val DateTime = TypeDefinition.Class( TypeDefinition.Class( name = "Date", properties = listOf( - TypeProperty("year", TypeDefinition.Primitive(JsonType.INTEGER), true), + TypeProperty("year", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), true), TypeProperty( - "month", TypeDefinition.Enum( + "month", + TypeDefinition.Enum( "Month", JsonType.STRING, listOf( "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" ) - ), true + ), + true ), - TypeProperty("day", TypeDefinition.Primitive(JsonType.INTEGER), true) + TypeProperty("day", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), true) ) ), true @@ -143,9 +324,17 @@ val DateTime = TypeDefinition.Class( TypeDefinition.Class( name = "Time", properties = listOf( - TypeProperty("hour", TypeDefinition.Primitive(JsonType.INTEGER), true), - TypeProperty("minute", TypeDefinition.Primitive(JsonType.INTEGER), true), - TypeProperty("seconds", TypeDefinition.Primitive(JsonType.INTEGER), true) + TypeProperty("hour", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), true), + TypeProperty( + "minute", + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + true + ), + TypeProperty( + "seconds", + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + true + ) ) ), true @@ -155,15 +344,15 @@ val DateTime = TypeDefinition.Class( val Demo = TypeDefinition.Class( name = "Demo", properties = listOf( - TypeProperty("s", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("i", TypeDefinition.Primitive(JsonType.INTEGER), false), - TypeProperty("n", TypeDefinition.Primitive(JsonType.NUMBER), false), - TypeProperty("b", TypeDefinition.Primitive(JsonType.BOOLEAN), false), + TypeProperty("s", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), + TypeProperty("i", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), false), + TypeProperty("n", TypeDefinition.Primitive(JsonPrimitiveType.NUMBER), false), + TypeProperty("b", TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), false), TypeProperty("l", TypeDefinition.Null(), false), - TypeProperty("ns", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("ni", TypeDefinition.Primitive(JsonType.INTEGER), true), - TypeProperty("nn", TypeDefinition.Primitive(JsonType.NUMBER), true), - TypeProperty("nb", TypeDefinition.Primitive(JsonType.BOOLEAN), true), + TypeProperty("ns", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("ni", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), true), + TypeProperty("nn", TypeDefinition.Primitive(JsonPrimitiveType.NUMBER), true), + TypeProperty("nb", TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), true), TypeProperty("nl", TypeDefinition.Null(), true) ) @@ -172,13 +361,13 @@ val Demo = TypeDefinition.Class( val Delivery = TypeDefinition.Class( name = "Delivery", properties = listOf( - TypeProperty("item", TypeDefinition.Primitive(JsonType.STRING), false), + TypeProperty("item", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), TypeProperty( "customer", TypeDefinition.Class( name = "Customer", properties = listOf( - TypeProperty("name", TypeDefinition.Primitive(JsonType.STRING), true), + TypeProperty("name", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), TypeProperty("billing_address", Address, true), TypeProperty("shipping_address", Address, true) ) @@ -188,18 +377,89 @@ val Delivery = TypeDefinition.Class( ) ) +val Employee = TypeDefinition.Class( + name = "Employee", + properties = listOf( + TypeProperty("name", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty( + name = "contact", + type = TypeDefinition.Class( + name = "Contact", + properties = listOf( + TypeProperty( + "phone", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + false + ), + TypeProperty("address", Address, false) + ) + ), + optional = true + ) + ) +) + val Foo = TypeDefinition.Class( name = "Foo", properties = listOf( - TypeProperty("bar", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("baz", TypeDefinition.Primitive(JsonType.INTEGER), true) + TypeProperty("bar", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("baz", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), true) ) ) val Location = TypeDefinition.Class( name = "Location", properties = listOf( - TypeProperty("planet", TypeDefinition.Constant(JsonType.STRING, "earth"), false) + TypeProperty("planet", TypeDefinition.Constant(JsonType.STRING, "earth"), false), + TypeProperty("solar_system", TypeDefinition.Constant(JsonType.STRING, "sol"), false) + ) +) + +val Message = TypeDefinition.Class( + name = "Message", + properties = listOf( + TypeProperty( + "destination", + TypeDefinition.Array(TypeDefinition.Primitive(JsonPrimitiveType.STRING)), + optional = false, + readOnly = true + ), + TypeProperty( + "origin", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + optional = false, + readOnly = true + ), + TypeProperty( + "subject", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + optional = true, + readOnly = true + ), + TypeProperty( + "message", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + optional = true, + readOnly = true + ), + TypeProperty( + "labels", + TypeDefinition.Array(TypeDefinition.Primitive(JsonPrimitiveType.STRING)), + optional = true, + readOnly = false + ), + TypeProperty( + "read", + TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), + optional = true, + readOnly = false + ), + TypeProperty( + "important", + TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), + optional = true, + readOnly = false + ) ) ) @@ -209,12 +469,12 @@ val Opus = TypeDefinition.Class( properties = listOf( TypeProperty( "title", - TypeDefinition.Primitive(JsonType.STRING, "The opus's title."), + TypeDefinition.Primitive(JsonPrimitiveType.STRING, "The opus's title."), true ), TypeProperty( "composer", - TypeDefinition.Primitive(JsonType.STRING, "The opus's composer."), + TypeDefinition.Primitive(JsonPrimitiveType.STRING, "The opus's composer."), true ), TypeProperty( @@ -226,7 +486,10 @@ val Opus = TypeDefinition.Class( properties = listOf( TypeProperty( "name", - TypeDefinition.Primitive(JsonType.STRING, "The artist's name."), + TypeDefinition.Primitive( + JsonPrimitiveType.STRING, + "The artist's name." + ), true ), TypeProperty( @@ -250,7 +513,7 @@ val Opus = TypeDefinition.Class( ), TypeProperty( "duration", - TypeDefinition.Primitive(JsonType.INTEGER, "The opus's duration in seconds"), + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER, "The opus's duration in seconds"), true ) ) @@ -259,25 +522,103 @@ val Opus = TypeDefinition.Class( val Person = TypeDefinition.Class( name = "Person", properties = listOf( - TypeProperty("firstName", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("lastName", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("age", TypeDefinition.Primitive(JsonType.INTEGER), true) + TypeProperty("firstName", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("lastName", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("age", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), true) ) ) val Product = TypeDefinition.Class( name = "Product", properties = listOf( - TypeProperty("productId", TypeDefinition.Primitive(JsonType.INTEGER), false), - TypeProperty("productName", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("price", TypeDefinition.Primitive(JsonType.NUMBER), false) + TypeProperty("productId", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), false), + TypeProperty("productName", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), + TypeProperty("price", TypeDefinition.Primitive(JsonPrimitiveType.NUMBER), false) + ) +) + +val Paper = TypeDefinition.Class( + name = "Paper", + properties = listOf( + TypeProperty("title", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), + TypeProperty( + "author", + TypeDefinition.Array(TypeDefinition.Primitive(JsonPrimitiveType.STRING)), + false + ) + ) +) + +val Bike = TypeDefinition.Class( + name = "Bike", + properties = listOf( + TypeProperty( + "productId", + TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + false, + defaultValue = 1.0 + ), + TypeProperty( + "productName", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + false + ), + TypeProperty( + "type", + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + true, + defaultValue = "road" + ), + TypeProperty( + "price", + TypeDefinition.Primitive(JsonPrimitiveType.NUMBER), + false, + defaultValue = 55.5 + ), + TypeProperty( + "frameMaterial", + TypeDefinition.Enum( + name = "FrameMaterial", + type = JsonType.STRING, + values = listOf( + "carbon", + "light_aluminium", + "iron" + ) + ), + true, + defaultValue = "light_aluminium" + ), + TypeProperty( + "inStock", + TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), + false, + defaultValue = true + ), + TypeProperty( + "color", + TypeDefinition.Enum( + name = "Color", + type = JsonType.STRING, + values = listOf( + "red", + "amber", + "green", + "dark_blue", + "lime green", + "sunburst-yellow" + ) + ), + false, + defaultValue = "lime green" + ) ) ) val Shipping = TypeDefinition.Class( name = "Shipping", properties = listOf( - TypeProperty("item", TypeDefinition.Primitive(JsonType.STRING), false), + TypeProperty("item", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), TypeProperty("destination", Address, false) ) ) @@ -286,11 +627,150 @@ val Style = TypeDefinition.Class( name = "Style", properties = listOf( TypeProperty( - "color", + name = "color", + type = TypeDefinition.Enum( + name = "Color", + type = null, + values = listOf( + "red", + "amber", + "green", + "dark_blue", + "lime green", + "sunburst-yellow", + null + ) + ), + optional = false + ) + ) +) + +val Household = TypeDefinition.Class( + name = "Household", + properties = listOf( + TypeProperty( + name = "pets", + type = TypeDefinition.Array( + items = TypeDefinition.OneOfClass( + name = "Animal", + options = listOf( + TypeDefinition.Class( + name = "Fish", + properties = listOf( + TypeProperty( + name = "water", + type = TypeDefinition.Enum( + "Water", + JsonType.STRING, + listOf("salt", "fresh") + ), + optional = false + ), + TypeProperty( + name = "size", + type = TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + optional = true + ) + ) + ), + TypeDefinition.Class( + name = "Bird", + properties = listOf( + TypeProperty( + name = "food", + type = TypeDefinition.Enum( + name = "Food", + type = JsonType.STRING, + values = listOf( + "fish", + "bird", + "rodent", + "insect", + "fruit", + "seeds", + "pollen" + ) + ), + optional = false + ), + TypeProperty( + "can_fly", + TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), + false + ) + ) + ) + ), + description = "A representation of the animal kingdom" + ) + ), + optional = true + ), + TypeProperty( + name = "situation", + type = TypeDefinition.OneOfClass( + name = "Situation", + options = listOf( + TypeDefinition.Class( + name = "Marriage", + properties = listOf( + TypeProperty( + name = "spouses", + type = TypeDefinition.Array( + items = TypeDefinition.Primitive(JsonPrimitiveType.STRING) + ), + optional = false + ) + ) + ), + TypeDefinition.Class( + name = "Cotenancy", + properties = listOf( + TypeProperty( + name = "roommates", + type = TypeDefinition.Array( + items = TypeDefinition.Primitive(JsonPrimitiveType.STRING) + ), + optional = false + ) + ) + ) + ) + ), + optional = true + ) + ) +) + +val Jacket = TypeDefinition.Class( + name = "Jacket", + properties = listOf( + TypeProperty( + "size", TypeDefinition.Enum( - "Color", - JsonType.STRING, - listOf("red", "amber", "green", "dark_blue") + "Size", + JsonType.NUMBER, + listOf("1", "2", "3", "4") + ), + defaultValue = 1.0, + optional = false + ) + ) +) + +val Order = TypeDefinition.Class( + name = "Order", + properties = listOf( + TypeProperty( + "sizes", + TypeDefinition.Array( + TypeDefinition.Enum( + "Size", + JsonType.STRING, + listOf("x small", "small", "medium", "large", "x large") + ), + uniqueItems = true ), false ) @@ -300,10 +780,10 @@ val Style = TypeDefinition.Class( val User = TypeDefinition.Class( name = "User", properties = listOf( - TypeProperty("username", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("host", TypeDefinition.Primitive(JsonType.STRING), false), - TypeProperty("firstname", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("lastname", TypeDefinition.Primitive(JsonType.STRING), false), + TypeProperty("username", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), + TypeProperty("host", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), + TypeProperty("firstname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("lastname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), TypeProperty( "contact_type", TypeDefinition.Enum( @@ -316,48 +796,216 @@ val User = TypeDefinition.Class( ) ) +val Country = TypeDefinition.Class( + name = "Country", + properties = listOf( + TypeProperty("name", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("continent", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("population", TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), true) + ) +) + +// both items have additionalProperties explicitly defined +val AdditionalPropsMerged = TypeDefinition.Class( + name = "AdditionalPropsMerged", + properties = listOf( + TypeProperty("email", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("phone", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty( + "info", + TypeDefinition.Class( + name = "Info", + properties = listOf( + TypeProperty("notes", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("source", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true) + ), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Class("?", emptyList()), + optional = true, + readOnly = false + ) + ), + true + ), + TypeProperty("firstname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("lastname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false) + ) +) + +// only one item has additionalProperties explicitly defined +val AdditionalPropsSingleMerge = TypeDefinition.Class( + name = "AdditionalPropsSingleMerge", + properties = listOf( + TypeProperty("email", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("phone", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty( + "info", + TypeDefinition.Class( + name = "Info", + properties = listOf( + TypeProperty("notes", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("source", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true) + ), + additionalProperties = TypeProperty( + name = "", + type = TypeDefinition.Class("?", emptyList()), + optional = true, + readOnly = false + ) + ), + true + ), + TypeProperty("firstname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("lastname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false) + ) +) + val UserMerged = TypeDefinition.Class( name = "UserMerged", properties = listOf( - TypeProperty("email", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("phone", TypeDefinition.Primitive(JsonType.STRING), true), + TypeProperty("email", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("phone", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), TypeProperty( "info", TypeDefinition.Class( name = "Info", properties = listOf( - TypeProperty("notes", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("source", TypeDefinition.Primitive(JsonType.STRING), true) + TypeProperty("notes", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("source", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true) ) ), true ), - TypeProperty("firstname", TypeDefinition.Primitive(JsonType.STRING), true), - TypeProperty("lastname", TypeDefinition.Primitive(JsonType.STRING), false) + TypeProperty("firstname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), true), + TypeProperty("lastname", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false) ) ) val Version = TypeDefinition.Class( name = "Version", properties = listOf( - TypeProperty("version", TypeDefinition.Constant(JsonType.INTEGER, 42.0), false), - TypeProperty("delta", TypeDefinition.Constant(JsonType.NUMBER, 3.1415), true) + TypeProperty("major", TypeDefinition.Constant(JsonType.INTEGER, 42.0), false), + TypeProperty("delta", TypeDefinition.Constant(JsonType.NUMBER, 3.1415), true), + TypeProperty( + "id", + TypeDefinition.Class( + name = "Id", + properties = listOf( + TypeProperty( + "serialNumber", + TypeDefinition.Constant(JsonType.NUMBER, 12112.0), + true + ) + ) + ), + false + ), + TypeProperty( + "date", + TypeDefinition.Class( + name = "Date", + properties = listOf( + TypeProperty( + "year", + TypeDefinition.Constant(JsonType.INTEGER, 2021.0), + true + ), + TypeProperty( + "month", + TypeDefinition.Constant(JsonType.INTEGER, 3.0), + true + ) + ) + ), + true + ) ) ) val Video = TypeDefinition.Class( name = "Video", properties = listOf( - TypeProperty("title", TypeDefinition.Primitive(JsonType.STRING), false), + TypeProperty("title", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), TypeProperty( "tags", - TypeDefinition.Array(TypeDefinition.Primitive(JsonType.STRING), uniqueItems = true), + TypeDefinition.Array( + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + uniqueItems = true + ), true ), TypeProperty( "links", - TypeDefinition.Array(TypeDefinition.Primitive(JsonType.STRING), uniqueItems = true), + TypeDefinition.Array( + TypeDefinition.Primitive(JsonPrimitiveType.STRING), + uniqueItems = true + ), true ) ) ) + +val WeirdCombo = TypeDefinition.Class( + name = "WeirdCombo", + properties = listOf( + TypeProperty( + name = "anything", + type = TypeDefinition.OneOfClass( + name = "Anything", + options = listOf( + TypeDefinition.Class( + name = "Fish", + properties = listOf( + TypeProperty( + name = "water", + type = TypeDefinition.Enum("Water", JsonType.STRING, listOf("salt", "fresh")), + optional = false + ), + TypeProperty( + name = "size", + type = TypeDefinition.Primitive(JsonPrimitiveType.INTEGER), + optional = true + ) + ) + ), + TypeDefinition.Class( + name = "Bird", + properties = listOf( + TypeProperty( + name = "food", + type = TypeDefinition.Enum( + name = "Food", + type = JsonType.STRING, + values = listOf( + "fish", + "bird", + "rodent", + "insect", + "fruit", + "seeds", + "pollen" + ) + ), + optional = false + ), + TypeProperty("can_fly", TypeDefinition.Primitive(JsonPrimitiveType.BOOLEAN), false) + ) + ), + TypeDefinition.Class( + name = "Paper", + properties = listOf( + TypeProperty("title", TypeDefinition.Primitive(JsonPrimitiveType.STRING), false), + TypeProperty( + "author", + TypeDefinition.Array(TypeDefinition.Primitive(JsonPrimitiveType.STRING)), + false + ) + ) + ) + ) + ), + optional = true + ) + ) +) diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/generator/FileGeneratorTest.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/generator/FileGeneratorTest.kt new file mode 100644 index 0000000000..7b824cdc2e --- /dev/null +++ b/buildSrc/src/test/kotlin/com/datadog/gradle/plugin/jsonschema/generator/FileGeneratorTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.gradle.plugin.jsonschema.generator + +import com.datadog.gradle.plugin.jsonschema.Animal +import com.datadog.gradle.plugin.jsonschema.Article +import com.datadog.gradle.plugin.jsonschema.Bike +import com.datadog.gradle.plugin.jsonschema.Book +import com.datadog.gradle.plugin.jsonschema.Comment +import com.datadog.gradle.plugin.jsonschema.Company +import com.datadog.gradle.plugin.jsonschema.Conflict +import com.datadog.gradle.plugin.jsonschema.Customer +import com.datadog.gradle.plugin.jsonschema.DateTime +import com.datadog.gradle.plugin.jsonschema.Delivery +import com.datadog.gradle.plugin.jsonschema.Demo +import com.datadog.gradle.plugin.jsonschema.Foo +import com.datadog.gradle.plugin.jsonschema.Household +import com.datadog.gradle.plugin.jsonschema.Jacket +import com.datadog.gradle.plugin.jsonschema.Location +import com.datadog.gradle.plugin.jsonschema.Message +import com.datadog.gradle.plugin.jsonschema.NoOpLogger +import com.datadog.gradle.plugin.jsonschema.Opus +import com.datadog.gradle.plugin.jsonschema.Order +import com.datadog.gradle.plugin.jsonschema.Paper +import com.datadog.gradle.plugin.jsonschema.Person +import com.datadog.gradle.plugin.jsonschema.Product +import com.datadog.gradle.plugin.jsonschema.Shipping +import com.datadog.gradle.plugin.jsonschema.Style +import com.datadog.gradle.plugin.jsonschema.TypeDefinition +import com.datadog.gradle.plugin.jsonschema.User +import com.datadog.gradle.plugin.jsonschema.UserMerged +import com.datadog.gradle.plugin.jsonschema.Version +import com.datadog.gradle.plugin.jsonschema.Video +import com.datadog.gradle.plugin.jsonschema.WeirdCombo +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths + +@RunWith(Parameterized::class) +class FileGeneratorTest( + private val inputType: TypeDefinition, + private val outputFile: String +) { + + @get:Rule + val tempFolderRule = TemporaryFolder() + + lateinit var tempDir: File + + @Test + fun `generates a Poko file`() { + tempDir = tempFolderRule.newFolder() + val clazz = FileGeneratorTest::class.java + val outputPath = clazz.getResource("/output/$outputFile.kt").file + val testedGenerator = FileGenerator(tempDir, "com.example.model", NoOpLogger()) + + testedGenerator.generate(inputType) + + val generatedFile = Files.find( + Paths.get(tempDir.toURI()), + Integer.MAX_VALUE, + { _, attrs -> attrs.isRegularFile } + ).findFirst().get().toFile() + + val generatedContent = generatedFile.readText(Charsets.UTF_8) + val expectedContent = File(outputPath).readText(Charsets.UTF_8) + + if (generatedContent != expectedContent) { + val genLines = generatedContent.lines() + val expLines = expectedContent.lines() + for (i in 0 until minOf(genLines.size, expLines.size)) { + if (genLines[i] != expLines[i]) { + System.err.println("--- GENERATED $outputFile.kt \n") + System.err.println(generatedContent) + throw AssertionError( + "File $outputFile generated from \n$inputType didn't match expectation:\n" + + "First error on line ${i + 1}:\n" + + "<<<<<<< EXPECTED\n" + + expLines[i] + + "\n=======\n" + + genLines[i] + + "\n>>>>>>> GENERATED\n" + ) + } + } + } + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{index}: {1}") + fun data(): Collection> { + return listOf( + arrayOf(Article, "Article"), + arrayOf(Animal, "Animal"), + arrayOf(Bike, "Bike"), + arrayOf(Book, "Book"), + arrayOf(Comment, "Comment"), + arrayOf(Company, "Company"), + arrayOf(Conflict, "Conflict"), + arrayOf(Customer, "Customer"), + arrayOf(DateTime, "DateTime"), + arrayOf(Delivery, "Delivery"), + arrayOf(Demo, "Demo"), + arrayOf(Foo, "Foo"), + arrayOf(Household, "Household"), + arrayOf(Jacket, "Jacket"), + arrayOf(Person, "Person"), + arrayOf(Location, "Location"), + arrayOf(Message, "Message"), + arrayOf(Order, "Order"), + arrayOf(Opus, "Opus"), + arrayOf(Paper, "Paper"), + arrayOf(Product, "Product"), + arrayOf(Shipping, "Shipping"), + arrayOf(Style, "Style"), + arrayOf(Order, "Order"), + arrayOf(User, "User"), + arrayOf(UserMerged, "UserMerged"), + arrayOf(Version, "Version"), + arrayOf(Video, "Video"), + arrayOf(WeirdCombo, "WeirdCombo") + ) + } + } +} diff --git a/buildSrc/src/test/kotlin/com/datadog/gradle/utils/VersionTest.kt b/buildSrc/src/test/kotlin/com/datadog/gradle/utils/VersionTest.kt index 2dc65c116c..8d05e10143 100644 --- a/buildSrc/src/test/kotlin/com/datadog/gradle/utils/VersionTest.kt +++ b/buildSrc/src/test/kotlin/com/datadog/gradle/utils/VersionTest.kt @@ -54,6 +54,13 @@ class VersionTest { assert(code == next - 1) { "expected code to be next - 1 = ${next - 1} but was $code (@next:$next)" } } + @Test + fun addNoSuffixForRelease() { + val name = Version(3, 12, 7, Version.Type.Release).name + val expected = "3.12.7" + assert(name == expected) { " expected name to be $expected but was $name" } + } + @Test fun addSuffixForRC() { val name = Version(3, 12, 7, Version.Type.ReleaseCandidate(1)).name @@ -74,4 +81,18 @@ class VersionTest { val expected = "3.12.7-alpha3" assert(name == expected) { " expected name to be $expected but was $name" } } + + @Test + fun addSuffixForDev() { + val name = Version(3, 12, 7, Version.Type.Dev).name + val expected = "3.12.7-dev" + assert(name == expected) { " expected name to be $expected but was $name" } + } + + @Test + fun addSuffixForSnapshot() { + val name = Version(4, 11, 5, Version.Type.Snapshot).name + val expected = "4.11.5-SNAPSHOT" + assert(name == expected) { " expected name to be $expected but was $name" } + } } diff --git a/buildSrc/src/test/kotlin/com/example/forgery/AnimalForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/AnimalForgeryFactory.kt new file mode 100644 index 0000000000..a3a9c9563a --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/AnimalForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Animal +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class AnimalForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): Animal { + return forge.anElementFrom( + Animal.Fish( + water = forge.aValueFrom(Animal.Water::class.java), + size = forge.aNullable { aPositiveLong() } + ), + Animal.Bird( + food = forge.aValueFrom(Animal.Food::class.java), + canFly = forge.aBool() + ) + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/ArticleForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/ArticleForgeryFactory.kt index ce8ff6b772..292e55c1bd 100644 --- a/buildSrc/src/test/kotlin/com/example/forgery/ArticleForgeryFactory.kt +++ b/buildSrc/src/test/kotlin/com/example/forgery/ArticleForgeryFactory.kt @@ -5,7 +5,6 @@ */ package com.example.forgery - import com.example.model.Article import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/buildSrc/src/test/kotlin/com/example/forgery/BikeForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/BikeForgeryFactory.kt new file mode 100644 index 0000000000..53ed1deecd --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/BikeForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Bike +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class BikeForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): Bike { + return Bike( + productId = forge.aLong(), + productName = forge.aString(), + type = forge.aNullable { aString() }, + price = forge.aNumber(), + frameMaterial = forge.aNullable { aValueFrom(Bike.FrameMaterial::class.java) }, + inStock = forge.aBool(), + color = forge.aValueFrom(Bike.Color::class.java) + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/CommentForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/CommentForgeryFactory.kt new file mode 100644 index 0000000000..aa68b97776 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/CommentForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Comment +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class CommentForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): Comment { + return Comment( + message = forge.aNullable { forge.anAlphabeticalString() }, + ratings = forge.aNullable { + Comment.Ratings( + global = aLong(), + additionalProperties = aMap { + anAlphabeticalString() to aLong() + }.toMutableMap() + ) + }, + flags = forge.aNullable { + Comment.Flags( + additionalProperties = aMap { + anAlphabeticalString() to aBool() + }.toMutableMap() + ) + }, + tags = forge.aNullable { + Comment.Tags( + additionalProperties = aMap { + anAlphabeticalString() to anHexadecimalString() + }.toMutableMap() + ) + } + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/CompanyForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/CompanyForgeryFactory.kt new file mode 100644 index 0000000000..ba5021229d --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/CompanyForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Company +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class CompanyForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): Company { + return Company( + name = forge.aNullable { forge.anAlphabeticalString() }, + ratings = forge.aNullable { + Company.Ratings( + global = aLong(), + additionalProperties = aMap { + anAlphabeticalString() to aLong() + }.toMutableMap() + ) + }, + information = forge.aNullable { + Company.Information( + forge.aNullable { forge.aLong() }, + forge.aNullable { forge.aLong() }, + additionalProperties = forge.aMap { + anAlphabeticalString() to aMap { anHexadecimalString() to aLong() } + }.toMutableMap() + ) + }, + additionalProperties = forge.aMap { + anAlphabeticalString() to aNullable { anHexadecimalString() } + }.toMutableMap() + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/ConflictForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/ConflictForgeryFactory.kt index 7d92e04102..8d04f3dd49 100644 --- a/buildSrc/src/test/kotlin/com/example/forgery/ConflictForgeryFactory.kt +++ b/buildSrc/src/test/kotlin/com/example/forgery/ConflictForgeryFactory.kt @@ -14,7 +14,7 @@ internal class ConflictForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): Conflict { return Conflict( type = forge.aNullable { - Conflict.Type( + Conflict.ConflictType( aNullable { anAlphabeticalString() } ) }, diff --git a/buildSrc/src/test/kotlin/com/example/forgery/DemoForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/DemoForgeryFactory.kt index 46ee8ccaaf..245e82b8c1 100644 --- a/buildSrc/src/test/kotlin/com/example/forgery/DemoForgeryFactory.kt +++ b/buildSrc/src/test/kotlin/com/example/forgery/DemoForgeryFactory.kt @@ -16,12 +16,12 @@ internal class DemoForgeryFactory : ForgeryFactory { return Demo( s = forge.anAlphabeticalString(), i = forge.aLong(), - n = forge.aDouble(), + n = forge.aNumber(), b = forge.aBool(), l = null, ns = forge.aNullable { anAlphabeticalString() }, ni = forge.aNullable { forge.aLong() }, - nn = forge.aNullable { forge.aDouble() }, + nn = forge.aNullable { forge.aNumber() }, nb = forge.aNullable { forge.aBool() }, nl = null ) diff --git a/buildSrc/src/test/kotlin/com/example/forgery/ForgeExt.kt b/buildSrc/src/test/kotlin/com/example/forgery/ForgeExt.kt new file mode 100644 index 0000000000..9b4fb21e9c --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/ForgeExt.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import fr.xgouchet.elmyr.Forge + +internal fun Forge.aNumber(): Number { + return anElementFrom(anInt(), aDouble(), aFloat(), aLong()) +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/ForgeryConfiguration.kt b/buildSrc/src/test/kotlin/com/example/forgery/ForgeryConfiguration.kt index 44f8b38f71..c029d87f43 100644 --- a/buildSrc/src/test/kotlin/com/example/forgery/ForgeryConfiguration.kt +++ b/buildSrc/src/test/kotlin/com/example/forgery/ForgeryConfiguration.kt @@ -11,22 +11,32 @@ import fr.xgouchet.elmyr.ForgeConfigurator internal class ForgeryConfiguration : ForgeConfigurator { override fun configure(forge: Forge) { + forge.addFactory(AnimalForgeryFactory()) forge.addFactory(ArticleForgeryFactory()) + forge.addFactory(BikeForgeryFactory()) forge.addFactory(BookForgeryFactory()) forge.addFactory(ConflictForgeryFactory()) + forge.addFactory(CommentForgeryFactory()) + forge.addFactory(CompanyForgeryFactory()) forge.addFactory(CustomerForgeryFactory()) forge.addFactory(DateTimeForgeryFactory()) forge.addFactory(DeliveryForgeryFactory()) forge.addFactory(DemoForgeryFactory()) forge.addFactory(FooForgeryFactory()) + forge.addFactory(HouseholdForgeryFactory()) + forge.addFactory(JacketForgeryFactory()) forge.addFactory(LocationForgeryFactory()) + forge.addFactory(MessageForgeryFactory()) forge.addFactory(OpusForgeryFactory()) + forge.addFactory(OrderForgeryFactory()) forge.addFactory(PersonForgeryFactory()) forge.addFactory(ProductForgeryFactory()) - forge.addFactory(UserForgeryFactory()) forge.addFactory(ShippingForgeryFactory()) forge.addFactory(StyleForgeryFactory()) + forge.addFactory(UserForgeryFactory()) + forge.addFactory(UserMergedForgeryFactory()) forge.addFactory(VersionForgeryFactory()) forge.addFactory(VideoForgeryFactory()) + forge.addFactory(WeirdComboForgeryFactory()) } } diff --git a/buildSrc/src/test/kotlin/com/example/forgery/HouseholdForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/HouseholdForgeryFactory.kt new file mode 100644 index 0000000000..78986a37c1 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/HouseholdForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Household +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class HouseholdForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): Household { + return Household( + pets = forge.aNullable { + aList { + anElementFrom( + Household.Animal.Fish( + water = forge.aValueFrom(Household.Water::class.java), + size = forge.aNullable { aPositiveLong() } + ), + Household.Animal.Bird( + food = forge.aValueFrom(Household.Food::class.java), + canFly = forge.aBool() + ) + ) + } + }, + situation = forge.anElementFrom( + Household.Situation.Marriage( + spouses = forge.aList { anAlphabeticalString() } + ), + Household.Situation.Cotenancy( + roommates = forge.aList { anAlphabeticalString() } + ) + ) + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/JacketForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/JacketForgeryFactory.kt new file mode 100644 index 0000000000..f542733694 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/JacketForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Jacket +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class JacketForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): Jacket { + return if (forge.aBool()) { + Jacket(size = forge.aValueFrom(Jacket.Size::class.java)) + } else { + Jacket() + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/MessageForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/MessageForgeryFactory.kt new file mode 100644 index 0000000000..d5d5079ffd --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/MessageForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Message +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class MessageForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): Message { + return Message( + destination = forge.aList { aStringMatching(EMAIL_REGEX) }, + origin = forge.aStringMatching(EMAIL_REGEX), + subject = forge.aNullable { anAlphabeticalString() }, + message = forge.aNullable { anAlphabeticalString() }, + labels = forge.aNullable { forge.aList { anAlphabeticalString() } }, + read = forge.aNullable { forge.aBool() }, + important = forge.aNullable { forge.aBool() } + ) + } + + companion object { + const val EMAIL_REGEX = "[a-z]{3,9}\\.[a-z]{3,9}@[a-z]{3,9}\\.[a-z]{3}" + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/OrderForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/OrderForgeryFactory.kt new file mode 100644 index 0000000000..5b16ebf7b9 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/OrderForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.Order +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class OrderForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): Order { + return Order( + sizes = forge.aList { forge.aValueFrom(Order.Size::class.java) }.toSet() + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/PersonForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/PersonForgeryFactory.kt index 012b69bbbc..6b23548962 100644 --- a/buildSrc/src/test/kotlin/com/example/forgery/PersonForgeryFactory.kt +++ b/buildSrc/src/test/kotlin/com/example/forgery/PersonForgeryFactory.kt @@ -15,7 +15,7 @@ internal class PersonForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): Person { return Person( firstName = forge.aNullable { anAlphabeticalString() }, - lastName = forge.aNullable { anAlphabeticalString() }, + lastName = forge.aNullable { anAlphabeticalString() }, age = forge.aNullable { aLong() } ) } diff --git a/buildSrc/src/test/kotlin/com/example/forgery/ProductForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/ProductForgeryFactory.kt index 8900ddee7f..f614413dc9 100644 --- a/buildSrc/src/test/kotlin/com/example/forgery/ProductForgeryFactory.kt +++ b/buildSrc/src/test/kotlin/com/example/forgery/ProductForgeryFactory.kt @@ -16,7 +16,7 @@ internal class ProductForgeryFactory : ForgeryFactory { return Product( productId = forge.aLong(), productName = forge.anAlphabeticalString(), - price = forge.aDouble(0.0) + price = forge.aNumber() ) } } diff --git a/buildSrc/src/test/kotlin/com/example/forgery/UserMergedForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/UserMergedForgeryFactory.kt new file mode 100644 index 0000000000..2023ff78c4 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/UserMergedForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.UserMerged +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class UserMergedForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): UserMerged { + return UserMerged( + email = forge.aNullable { aStringMatching("\\w+@[a-z]+\\.[a-z]{3}") }, + phone = forge.aNullable { aStringMatching("\\d{3,8}") }, + info = forge.aNullable { + UserMerged.Info( + notes = forge.aNullable { anAlphabeticalString() }, + source = forge.aNullable { anAlphabeticalString() } + ) + }, + firstname = forge.aNullable { anAlphabeticalString() }, + lastname = forge.anAlphabeticalString() + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/forgery/VersionForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/VersionForgeryFactory.kt index a6aba77d9e..c6089fa403 100644 --- a/buildSrc/src/test/kotlin/com/example/forgery/VersionForgeryFactory.kt +++ b/buildSrc/src/test/kotlin/com/example/forgery/VersionForgeryFactory.kt @@ -12,6 +12,9 @@ import fr.xgouchet.elmyr.ForgeryFactory internal class VersionForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): Version { - return Version() + return Version( + Version.Id(), + forge.aNullable { Version.Date() } + ) } } diff --git a/buildSrc/src/test/kotlin/com/example/forgery/WeirdComboForgeryFactory.kt b/buildSrc/src/test/kotlin/com/example/forgery/WeirdComboForgeryFactory.kt new file mode 100644 index 0000000000..9d482d8cdc --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/forgery/WeirdComboForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.example.forgery + +import com.example.model.WeirdCombo +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class WeirdComboForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): WeirdCombo { + return WeirdCombo( + anything = forge.anElementFrom( + WeirdCombo.Anything.Fish( + water = forge.aValueFrom(WeirdCombo.Water::class.java), + size = forge.aNullable { aPositiveLong() } + ), + WeirdCombo.Anything.Bird( + food = forge.aValueFrom(WeirdCombo.Food::class.java), + canFly = forge.aBool() + ), + WeirdCombo.Anything.Paper( + title = forge.aString(), + author = forge.aList { aString() } + ) + ) + ) + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Animal.kt b/buildSrc/src/test/kotlin/com/example/model/Animal.kt new file mode 100644 index 0000000000..47a0e97805 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Animal.kt @@ -0,0 +1,213 @@ +package com.example.model + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Boolean +import kotlin.Long +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +/** + * A representation of the animal kingdom + */ +public sealed class Animal { + public abstract fun toJson(): JsonElement + + public data class Fish( + public val water: Water, + public val size: Long? = null, + ) : Animal() { + override fun toJson(): JsonElement { + val json = JsonObject() + json.add("water", water.toJson()) + size?.let { sizeNonNull -> + json.addProperty("size", sizeNonNull) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Fish { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Fish { + try { + val water = Water.fromJson(jsonObject.get("water").asString) + val size = jsonObject.get("size")?.asLong + return Fish(water, size) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } + } + } + } + + public data class Bird( + public val food: Food, + public val canFly: Boolean, + ) : Animal() { + override fun toJson(): JsonElement { + val json = JsonObject() + json.add("food", food.toJson()) + json.addProperty("can_fly", canFly) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Bird { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Bird { + try { + val food = Food.fromJson(jsonObject.get("food").asString) + val canFly = jsonObject.get("can_fly").asBoolean + return Bird(food, canFly) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } + } + } + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Animal { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into one of type Animal", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Animal { + val errors = mutableListOf() + val asFish = try { + Fish.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val asBird = try { + Bird.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val result = arrayOf( + asFish, + asBird, + ).firstOrNull { it != null } + if (result == null) { + val message = "Unable to parse json into one of type \n" + "Animal\n" + + errors.joinToString("\n") { it.message.toString() } + throw JsonParseException(message) + } + return result + } + } + + public enum class Water( + private val jsonValue: String, + ) { + SALT("salt"), + FRESH("fresh"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Water = values().first { + it.jsonValue == jsonString + } + } + } + + public enum class Food( + private val jsonValue: String, + ) { + FISH("fish"), + BIRD("bird"), + RODENT("rodent"), + INSECT("insect"), + FRUIT("fruit"), + SEEDS("seeds"), + POLLEN("pollen"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Food = values().first { + it.jsonValue == jsonString + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Article.kt b/buildSrc/src/test/kotlin/com/example/model/Article.kt index 5ce7f69516..c3ff7e28eb 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Article.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Article.kt @@ -3,20 +3,28 @@ package com.example.model import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.collections.ArrayList import kotlin.collections.List +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Article( - val title: String, - val tags: List? = null, - val authors: List +public data class Article( + public val title: String, + public val tags: List? = null, + public val authors: List, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("title", title) - if (tags != null) { - val tagsArray = JsonArray(tags.size) - tags.forEach { tagsArray.add(it) } + tags?.let { tagsNonNull -> + val tagsArray = JsonArray(tagsNonNull.size) + tagsNonNull.forEach { tagsArray.add(it) } json.add("tags", tagsArray) } val authorsArray = JsonArray(authors.size) @@ -24,4 +32,58 @@ internal data class Article( json.add("authors", authorsArray) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Article { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Article", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Article { + try { + val title = jsonObject.get("title").asString + val tags = jsonObject.get("tags")?.asJsonArray?.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + val authors = jsonObject.get("authors").asJsonArray.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + return Article(title, tags, authors) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Article", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Article", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Article", + e + ) + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Bike.kt b/buildSrc/src/test/kotlin/com/example/model/Bike.kt new file mode 100644 index 0000000000..a6ff21f2ba --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Bike.kt @@ -0,0 +1,129 @@ +package com.example.model + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Boolean +import kotlin.Long +import kotlin.Number +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Bike( + public val productId: Long = 1L, + public val productName: String, + public val type: String? = "road", + public val price: Number = 55.5, + public val frameMaterial: FrameMaterial? = FrameMaterial.LIGHT_ALUMINIUM, + public val inStock: Boolean = true, + public val color: Color = Color.LIME_GREEN, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + json.addProperty("productId", productId) + json.addProperty("productName", productName) + type?.let { typeNonNull -> + json.addProperty("type", typeNonNull) + } + json.addProperty("price", price) + frameMaterial?.let { frameMaterialNonNull -> + json.add("frameMaterial", frameMaterialNonNull.toJson()) + } + json.addProperty("inStock", inStock) + json.add("color", color.toJson()) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Bike { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bike", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Bike { + try { + val productId = jsonObject.get("productId").asLong + val productName = jsonObject.get("productName").asString + val type = jsonObject.get("type")?.asString + val price = jsonObject.get("price").asNumber + val frameMaterial = jsonObject.get("frameMaterial")?.asString?.let { + FrameMaterial.fromJson(it) + } + val inStock = jsonObject.get("inStock").asBoolean + val color = Color.fromJson(jsonObject.get("color").asString) + return Bike(productId, productName, type, price, frameMaterial, inStock, color) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bike", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Bike", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Bike", + e + ) + } + } + } + + public enum class FrameMaterial( + private val jsonValue: String, + ) { + CARBON("carbon"), + LIGHT_ALUMINIUM("light_aluminium"), + IRON("iron"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): FrameMaterial = values().first { + it.jsonValue == jsonString + } + } + } + + public enum class Color( + private val jsonValue: String, + ) { + RED("red"), + AMBER("amber"), + GREEN("green"), + DARK_BLUE("dark_blue"), + LIME_GREEN("lime green"), + SUNBURST_YELLOW("sunburst-yellow"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Color = values().first { + it.jsonValue == jsonString + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Book.kt b/buildSrc/src/test/kotlin/com/example/model/Book.kt index 6afc7fb972..95a68736fc 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Book.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Book.kt @@ -2,17 +2,24 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject -import kotlin.Double +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Long +import kotlin.Number import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Book( - val bookId: Long, - val title: String, - val price: Double, - val author: Author +public data class Book( + public val bookId: Long, + public val title: String, + public val price: Number, + public val author: Author, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("bookId", bookId) json.addProperty("title", title) @@ -21,29 +28,163 @@ internal data class Book( return json } - data class Author( - val firstName: String, - val lastName: String, - val contact: Contact + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Book { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Book", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Book { + try { + val bookId = jsonObject.get("bookId").asLong + val title = jsonObject.get("title").asString + val price = jsonObject.get("price").asNumber + val author = jsonObject.get("author").asJsonObject.let { + Author.fromJsonObject(it) + } + return Book(bookId, title, price, author) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Book", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Book", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Book", + e + ) + } + } + } + + public data class Author( + public val firstName: String, + public val lastName: String, + public val contact: Contact, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("firstName", firstName) json.addProperty("lastName", lastName) json.add("contact", contact.toJson()) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Author { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Author", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Author { + try { + val firstName = jsonObject.get("firstName").asString + val lastName = jsonObject.get("lastName").asString + val contact = jsonObject.get("contact").asJsonObject.let { + Contact.fromJsonObject(it) + } + return Author(firstName, lastName, contact) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Author", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Author", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Author", + e + ) + } + } + } } - data class Contact( - val phone: String? = null, - val email: String? = null + public data class Contact( + public val phone: String? = null, + public val email: String? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (phone != null) json.addProperty("phone", phone) - if (email != null) json.addProperty("email", email) + phone?.let { phoneNonNull -> + json.addProperty("phone", phoneNonNull) + } + email?.let { emailNonNull -> + json.addProperty("email", emailNonNull) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Contact { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Contact", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Contact { + try { + val phone = jsonObject.get("phone")?.asString + val email = jsonObject.get("email")?.asString + return Contact(phone, email) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Contact", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Contact", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Contact", + e + ) + } + } + } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Comment.kt b/buildSrc/src/test/kotlin/com/example/model/Comment.kt new file mode 100644 index 0000000000..68772f083e --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Comment.kt @@ -0,0 +1,264 @@ +package com.example.model + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Array +import kotlin.Boolean +import kotlin.Long +import kotlin.String +import kotlin.collections.Map +import kotlin.collections.MutableMap +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Comment( + public val message: String? = null, + public val ratings: Ratings? = null, + public val flags: Flags? = null, + public val tags: Tags? = null, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + message?.let { messageNonNull -> + json.addProperty("message", messageNonNull) + } + ratings?.let { ratingsNonNull -> + json.add("ratings", ratingsNonNull.toJson()) + } + flags?.let { flagsNonNull -> + json.add("flags", flagsNonNull.toJson()) + } + tags?.let { tagsNonNull -> + json.add("tags", tagsNonNull.toJson()) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Comment { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Comment", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Comment { + try { + val message = jsonObject.get("message")?.asString + val ratings = jsonObject.get("ratings")?.asJsonObject?.let { + Ratings.fromJsonObject(it) + } + val flags = jsonObject.get("flags")?.asJsonObject?.let { + Flags.fromJsonObject(it) + } + val tags = jsonObject.get("tags")?.asJsonObject?.let { + Tags.fromJsonObject(it) + } + return Comment(message, ratings, flags, tags) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Comment", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Comment", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Comment", + e + ) + } + } + } + + public data class Ratings( + public val global: Long, + public val additionalProperties: Map = mapOf(), + ) { + public fun toJson(): JsonElement { + val json = JsonObject() + json.addProperty("global", global) + additionalProperties.forEach { (k, v) -> + if (k !in RESERVED_PROPERTIES) { + json.addProperty(k, v) + } + } + return json + } + + public companion object { + internal val RESERVED_PROPERTIES: Array = arrayOf("global") + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Ratings { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Ratings { + try { + val global = jsonObject.get("global").asLong + val additionalProperties = mutableMapOf() + for (entry in jsonObject.entrySet()) { + if (entry.key !in RESERVED_PROPERTIES) { + additionalProperties[entry.key] = entry.value.asLong + } + } + return Ratings(global, additionalProperties) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } + } + } + } + + public data class Flags( + public val additionalProperties: MutableMap = mutableMapOf(), + ) { + public fun toJson(): JsonElement { + val json = JsonObject() + additionalProperties.forEach { (k, v) -> + json.addProperty(k, v) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Flags { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Flags", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Flags { + try { + val additionalProperties = mutableMapOf() + for (entry in jsonObject.entrySet()) { + additionalProperties[entry.key] = entry.value.asBoolean + } + return Flags(additionalProperties) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Flags", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Flags", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Flags", + e + ) + } + } + } + } + + public data class Tags( + public val additionalProperties: MutableMap = mutableMapOf(), + ) { + public fun toJson(): JsonElement { + val json = JsonObject() + additionalProperties.forEach { (k, v) -> + json.addProperty(k, v) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Tags { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Tags", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Tags { + try { + val additionalProperties = mutableMapOf() + for (entry in jsonObject.entrySet()) { + additionalProperties[entry.key] = entry.value.asString + } + return Tags(additionalProperties) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Tags", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Tags", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Tags", + e + ) + } + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Company.kt b/buildSrc/src/test/kotlin/com/example/model/Company.kt new file mode 100644 index 0000000000..00c423756a --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Company.kt @@ -0,0 +1,232 @@ +package com.example.model + +import com.datadog.android.core.`internal`.utils.JsonSerializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Any +import kotlin.Array +import kotlin.Long +import kotlin.String +import kotlin.collections.MutableMap +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Company( + public val name: String? = null, + public val ratings: Ratings? = null, + public val information: Information? = null, + public val additionalProperties: MutableMap = mutableMapOf(), +) { + public fun toJson(): JsonElement { + val json = JsonObject() + name?.let { nameNonNull -> + json.addProperty("name", nameNonNull) + } + ratings?.let { ratingsNonNull -> + json.add("ratings", ratingsNonNull.toJson()) + } + information?.let { informationNonNull -> + json.add("information", informationNonNull.toJson()) + } + additionalProperties.forEach { (k, v) -> + if (k !in RESERVED_PROPERTIES) { + json.add(k, JsonSerializer.toJsonElement(v)) + } + } + return json + } + + public companion object { + internal val RESERVED_PROPERTIES: Array = arrayOf("name", "ratings", "information") + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Company { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Company", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Company { + try { + val name = jsonObject.get("name")?.asString + val ratings = jsonObject.get("ratings")?.asJsonObject?.let { + Ratings.fromJsonObject(it) + } + val information = jsonObject.get("information")?.asJsonObject?.let { + Information.fromJsonObject(it) + } + val additionalProperties = mutableMapOf() + for (entry in jsonObject.entrySet()) { + if (entry.key !in RESERVED_PROPERTIES) { + additionalProperties[entry.key] = entry.value + } + } + return Company(name, ratings, information, additionalProperties) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Company", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Company", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Company", + e + ) + } + } + } + + public data class Ratings( + public val global: Long, + public val additionalProperties: MutableMap = mutableMapOf(), + ) { + public fun toJson(): JsonElement { + val json = JsonObject() + json.addProperty("global", global) + additionalProperties.forEach { (k, v) -> + if (k !in RESERVED_PROPERTIES) { + json.addProperty(k, v) + } + } + return json + } + + public companion object { + internal val RESERVED_PROPERTIES: Array = arrayOf("global") + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Ratings { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Ratings { + try { + val global = jsonObject.get("global").asLong + val additionalProperties = mutableMapOf() + for (entry in jsonObject.entrySet()) { + if (entry.key !in RESERVED_PROPERTIES) { + additionalProperties[entry.key] = entry.value.asLong + } + } + return Ratings(global, additionalProperties) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Ratings", + e + ) + } + } + } + } + + public data class Information( + public val date: Long? = null, + public val priority: Long? = null, + public val additionalProperties: MutableMap = mutableMapOf(), + ) { + public fun toJson(): JsonElement { + val json = JsonObject() + date?.let { dateNonNull -> + json.addProperty("date", dateNonNull) + } + priority?.let { priorityNonNull -> + json.addProperty("priority", priorityNonNull) + } + additionalProperties.forEach { (k, v) -> + if (k !in RESERVED_PROPERTIES) { + json.add(k, JsonSerializer.toJsonElement(v)) + } + } + return json + } + + public companion object { + internal val RESERVED_PROPERTIES: Array = arrayOf("date", "priority") + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Information { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Information", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Information { + try { + val date = jsonObject.get("date")?.asLong + val priority = jsonObject.get("priority")?.asLong + val additionalProperties = mutableMapOf() + for (entry in jsonObject.entrySet()) { + if (entry.key !in RESERVED_PROPERTIES) { + additionalProperties[entry.key] = entry.value + } + } + return Information(date, priority, additionalProperties) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Information", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Information", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Information", + e + ) + } + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Conflict.kt b/buildSrc/src/test/kotlin/com/example/model/Conflict.kt index d0c4d3ac2d..e3d9eb901d 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Conflict.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Conflict.kt @@ -2,53 +2,202 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Conflict( - val type: Type? = null, - val user: User? = null +public data class Conflict( + public val type: ConflictType? = null, + public val user: User? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (type != null) json.add("type", type.toJson()) - if (user != null) json.add("user", user.toJson()) + type?.let { typeNonNull -> + json.add("type", typeNonNull.toJson()) + } + user?.let { userNonNull -> + json.add("user", userNonNull.toJson()) + } return json } - data class Type( - val id: String? = null + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Conflict { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Conflict", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Conflict { + try { + val type = jsonObject.get("type")?.asJsonObject?.let { + ConflictType.fromJsonObject(it) + } + val user = jsonObject.get("user")?.asJsonObject?.let { + User.fromJsonObject(it) + } + return Conflict(type, user) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Conflict", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Conflict", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Conflict", + e + ) + } + } + } + + public data class ConflictType( + public val id: String? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (id != null) json.addProperty("id", id) + id?.let { idNonNull -> + json.addProperty("id", idNonNull) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): ConflictType { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type ConflictType", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): ConflictType { + try { + val id = jsonObject.get("id")?.asString + return ConflictType(id) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type ConflictType", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type ConflictType", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type ConflictType", + e + ) + } + } + } } - data class User( - val name: String? = null, - val type: Type1? = null + public data class User( + public val name: String? = null, + public val type: UserType? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (name != null) json.addProperty("name", name) - if (type != null) json.add("type", type.toJson()) + name?.let { nameNonNull -> + json.addProperty("name", nameNonNull) + } + type?.let { typeNonNull -> + json.add("type", typeNonNull.toJson()) + } return json } - } - enum class Type1 { - UNKNOWN, + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): User { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } + } - CUSTOMER, + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): User { + try { + val name = jsonObject.get("name")?.asString + val type = jsonObject.get("type")?.asString?.let { + UserType.fromJson(it) + } + return User(name, type) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } + } + } + } + + public enum class UserType( + private val jsonValue: String, + ) { + UNKNOWN("unknown"), + CUSTOMER("customer"), + PARTNER("partner"), + ; - PARTNER; + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) - fun toJson(): JsonElement = when (this) { - UNKNOWN -> JsonPrimitive("unknown") - CUSTOMER -> JsonPrimitive("customer") - PARTNER -> JsonPrimitive("partner") + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): UserType = values().first { + it.jsonValue == jsonString + } } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Customer.kt b/buildSrc/src/test/kotlin/com/example/model/Customer.kt index 134a5d5fda..d8c25a6eb9 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Customer.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Customer.kt @@ -2,32 +2,133 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Customer( - val name: String? = null, - val billingAddress: Address? = null, - val shippingAddress: Address? = null +public data class Customer( + public val name: String? = null, + public val billingAddress: Address? = null, + public val shippingAddress: Address? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (name != null) json.addProperty("name", name) - if (billingAddress != null) json.add("billing_address", billingAddress.toJson()) - if (shippingAddress != null) json.add("shipping_address", shippingAddress.toJson()) + name?.let { nameNonNull -> + json.addProperty("name", nameNonNull) + } + billingAddress?.let { billingAddressNonNull -> + json.add("billing_address", billingAddressNonNull.toJson()) + } + shippingAddress?.let { shippingAddressNonNull -> + json.add("shipping_address", shippingAddressNonNull.toJson()) + } return json } - data class Address( - val streetAddress: String, - val city: String, - val state: String + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Customer { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Customer { + try { + val name = jsonObject.get("name")?.asString + val billingAddress = jsonObject.get("billing_address")?.asJsonObject?.let { + Address.fromJsonObject(it) + } + val shippingAddress = jsonObject.get("shipping_address")?.asJsonObject?.let { + Address.fromJsonObject(it) + } + return Customer(name, billingAddress, shippingAddress) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } + } + } + + public data class Address( + public val streetAddress: String, + public val city: String, + public val state: String, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("street_address", streetAddress) json.addProperty("city", city) json.addProperty("state", state) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Address { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Address { + try { + val streetAddress = jsonObject.get("street_address").asString + val city = jsonObject.get("city").asString + val state = jsonObject.get("state").asString + return Address(streetAddress, city, state) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } + } + } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/DateTime.kt b/buildSrc/src/test/kotlin/com/example/model/DateTime.kt index 9532bc4871..149ab1bebc 100644 --- a/buildSrc/src/test/kotlin/com/example/model/DateTime.kt +++ b/buildSrc/src/test/kotlin/com/example/model/DateTime.kt @@ -2,86 +2,227 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Long +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class DateTime( - val date: Date? = null, - val time: Time? = null +public data class DateTime( + public val date: Date? = null, + public val time: Time? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (date != null) json.add("date", date.toJson()) - if (time != null) json.add("time", time.toJson()) + date?.let { dateNonNull -> + json.add("date", dateNonNull.toJson()) + } + time?.let { timeNonNull -> + json.add("time", timeNonNull.toJson()) + } return json } - data class Date( - val year: Long? = null, - val month: Month? = null, - val day: Long? = null + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): DateTime { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type DateTime", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): DateTime { + try { + val date = jsonObject.get("date")?.asJsonObject?.let { + Date.fromJsonObject(it) + } + val time = jsonObject.get("time")?.asJsonObject?.let { + Time.fromJsonObject(it) + } + return DateTime(date, time) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type DateTime", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type DateTime", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type DateTime", + e + ) + } + } + } + + public data class Date( + public val year: Long? = null, + public val month: Month? = null, + public val day: Long? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (year != null) json.addProperty("year", year) - if (month != null) json.add("month", month.toJson()) - if (day != null) json.addProperty("day", day) + year?.let { yearNonNull -> + json.addProperty("year", yearNonNull) + } + month?.let { monthNonNull -> + json.add("month", monthNonNull.toJson()) + } + day?.let { dayNonNull -> + json.addProperty("day", dayNonNull) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Date { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Date { + try { + val year = jsonObject.get("year")?.asLong + val month = jsonObject.get("month")?.asString?.let { + Month.fromJson(it) + } + val day = jsonObject.get("day")?.asLong + return Date(year, month, day) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } + } + } } - data class Time( - val hour: Long? = null, - val minute: Long? = null, - val seconds: Long? = null + public data class Time( + public val hour: Long? = null, + public val minute: Long? = null, + public val seconds: Long? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (hour != null) json.addProperty("hour", hour) - if (minute != null) json.addProperty("minute", minute) - if (seconds != null) json.addProperty("seconds", seconds) + hour?.let { hourNonNull -> + json.addProperty("hour", hourNonNull) + } + minute?.let { minuteNonNull -> + json.addProperty("minute", minuteNonNull) + } + seconds?.let { secondsNonNull -> + json.addProperty("seconds", secondsNonNull) + } return json } - } - - enum class Month { - JAN, - - FEB, - MAR, - - APR, - - MAY, - - JUN, - - JUL, - - AUG, - - SEP, - - OCT, - - NOV, - - DEC; + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Time { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Time", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Time { + try { + val hour = jsonObject.get("hour")?.asLong + val minute = jsonObject.get("minute")?.asLong + val seconds = jsonObject.get("seconds")?.asLong + return Time(hour, minute, seconds) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Time", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Time", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Time", + e + ) + } + } + } + } - fun toJson(): JsonElement = when (this) { - JAN -> JsonPrimitive("jan") - FEB -> JsonPrimitive("feb") - MAR -> JsonPrimitive("mar") - APR -> JsonPrimitive("apr") - MAY -> JsonPrimitive("may") - JUN -> JsonPrimitive("jun") - JUL -> JsonPrimitive("jul") - AUG -> JsonPrimitive("aug") - SEP -> JsonPrimitive("sep") - OCT -> JsonPrimitive("oct") - NOV -> JsonPrimitive("nov") - DEC -> JsonPrimitive("dec") + public enum class Month( + private val jsonValue: String, + ) { + JAN("jan"), + FEB("feb"), + MAR("mar"), + APR("apr"), + MAY("may"), + JUN("jun"), + JUL("jul"), + AUG("aug"), + SEP("sep"), + OCT("oct"), + NOV("nov"), + DEC("dec"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Month = values().first { + it.jsonValue == jsonString + } } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Delivery.kt b/buildSrc/src/test/kotlin/com/example/model/Delivery.kt index 2fd15dc5cf..d53272e936 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Delivery.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Delivery.kt @@ -2,44 +2,188 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Delivery( - val item: String, - val customer: Customer +public data class Delivery( + public val item: String, + public val customer: Customer, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("item", item) json.add("customer", customer.toJson()) return json } - data class Customer( - val name: String? = null, - val billingAddress: Address? = null, - val shippingAddress: Address? = null + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Delivery { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Delivery", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Delivery { + try { + val item = jsonObject.get("item").asString + val customer = jsonObject.get("customer").asJsonObject.let { + Customer.fromJsonObject(it) + } + return Delivery(item, customer) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Delivery", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Delivery", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Delivery", + e + ) + } + } + } + + public data class Customer( + public val name: String? = null, + public val billingAddress: Address? = null, + public val shippingAddress: Address? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (name != null) json.addProperty("name", name) - if (billingAddress != null) json.add("billing_address", billingAddress.toJson()) - if (shippingAddress != null) json.add("shipping_address", shippingAddress.toJson()) + name?.let { nameNonNull -> + json.addProperty("name", nameNonNull) + } + billingAddress?.let { billingAddressNonNull -> + json.add("billing_address", billingAddressNonNull.toJson()) + } + shippingAddress?.let { shippingAddressNonNull -> + json.add("shipping_address", shippingAddressNonNull.toJson()) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Customer { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Customer { + try { + val name = jsonObject.get("name")?.asString + val billingAddress = jsonObject.get("billing_address")?.asJsonObject?.let { + Address.fromJsonObject(it) + } + val shippingAddress = jsonObject.get("shipping_address")?.asJsonObject?.let { + Address.fromJsonObject(it) + } + return Customer(name, billingAddress, shippingAddress) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Customer", + e + ) + } + } + } } - data class Address( - val streetAddress: String, - val city: String, - val state: String + public data class Address( + public val streetAddress: String, + public val city: String, + public val state: String, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("street_address", streetAddress) json.addProperty("city", city) json.addProperty("state", state) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Address { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Address { + try { + val streetAddress = jsonObject.get("street_address").asString + val city = jsonObject.get("city").asString + val state = jsonObject.get("state").asString + return Address(streetAddress, city, state) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } + } + } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Demo.kt b/buildSrc/src/test/kotlin/com/example/model/Demo.kt index 42e01420d0..d957fbb1ee 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Demo.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Demo.kt @@ -1,36 +1,102 @@ package com.example.model import com.google.gson.JsonElement +import com.google.gson.JsonNull import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Boolean -import kotlin.Double import kotlin.Long import kotlin.Nothing +import kotlin.Number import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Demo( - val s: String, - val i: Long, - val n: Double, - val b: Boolean, - val l: Nothing? = null, - val ns: String? = null, - val ni: Long? = null, - val nn: Double? = null, - val nb: Boolean? = null, - val nl: Nothing? = null +public data class Demo( + public val s: String, + public val i: Long, + public val n: Number, + public val b: Boolean, + public val l: Nothing? = null, + public val ns: String? = null, + public val ni: Long? = null, + public val nn: Number? = null, + public val nb: Boolean? = null, + public val nl: Nothing? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("s", s) json.addProperty("i", i) json.addProperty("n", n) json.addProperty("b", b) - json.add("l", null) - if (ns != null) json.addProperty("ns", ns) - if (ni != null) json.addProperty("ni", ni) - if (nn != null) json.addProperty("nn", nn) - if (nb != null) json.addProperty("nb", nb) + json.add("l", JsonNull.INSTANCE) + ns?.let { nsNonNull -> + json.addProperty("ns", nsNonNull) + } + ni?.let { niNonNull -> + json.addProperty("ni", niNonNull) + } + nn?.let { nnNonNull -> + json.addProperty("nn", nnNonNull) + } + nb?.let { nbNonNull -> + json.addProperty("nb", nbNonNull) + } + json.add("nl", JsonNull.INSTANCE) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Demo { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Demo", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Demo { + try { + val s = jsonObject.get("s").asString + val i = jsonObject.get("i").asLong + val n = jsonObject.get("n").asNumber + val b = jsonObject.get("b").asBoolean + val l = null + val ns = jsonObject.get("ns")?.asString + val ni = jsonObject.get("ni")?.asLong + val nn = jsonObject.get("nn")?.asNumber + val nb = jsonObject.get("nb")?.asBoolean + val nl = null + return Demo(s, i, n, b, l, ns, ni, nn, nb, nl) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Demo", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Demo", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Demo", + e + ) + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Foo.kt b/buildSrc/src/test/kotlin/com/example/model/Foo.kt index c089bdc5d4..b450e616d9 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Foo.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Foo.kt @@ -2,17 +2,69 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Long import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Foo( - val bar: String? = null, - val baz: Long? = null +public data class Foo( + public val bar: String? = null, + public val baz: Long? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (bar != null) json.addProperty("bar", bar) - if (baz != null) json.addProperty("baz", baz) + bar?.let { barNonNull -> + json.addProperty("bar", barNonNull) + } + baz?.let { bazNonNull -> + json.addProperty("baz", bazNonNull) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Foo { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Foo", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Foo { + try { + val bar = jsonObject.get("bar")?.asString + val baz = jsonObject.get("baz")?.asLong + return Foo(bar, baz) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Foo", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Foo", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Foo", + e + ) + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Household.kt b/buildSrc/src/test/kotlin/com/example/model/Household.kt new file mode 100644 index 0000000000..c73a580c15 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Household.kt @@ -0,0 +1,447 @@ +package com.example.model + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Boolean +import kotlin.Long +import kotlin.String +import kotlin.collections.ArrayList +import kotlin.collections.List +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Household( + public val pets: List? = null, + public val situation: Situation? = null, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + pets?.let { petsNonNull -> + val petsArray = JsonArray(petsNonNull.size) + petsNonNull.forEach { petsArray.add(it.toJson()) } + json.add("pets", petsArray) + } + situation?.let { situationNonNull -> + json.add("situation", situationNonNull.toJson()) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Household { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Household", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Household { + try { + val pets = jsonObject.get("pets")?.asJsonArray?.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(Animal.fromJsonObject(it.asJsonObject)) + } + collection + } + val situation = jsonObject.get("situation")?.asJsonObject?.let { + Situation.fromJsonObject(it) + } + return Household(pets, situation) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Household", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Household", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Household", + e + ) + } + } + } + + /** + * A representation of the animal kingdom + */ + public sealed class Animal { + public abstract fun toJson(): JsonElement + + public data class Fish( + public val water: Water, + public val size: Long? = null, + ) : Animal() { + override fun toJson(): JsonElement { + val json = JsonObject() + json.add("water", water.toJson()) + size?.let { sizeNonNull -> + json.addProperty("size", sizeNonNull) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Fish { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Fish { + try { + val water = Water.fromJson(jsonObject.get("water").asString) + val size = jsonObject.get("size")?.asLong + return Fish(water, size) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } + } + } + } + + public data class Bird( + public val food: Food, + public val canFly: Boolean, + ) : Animal() { + override fun toJson(): JsonElement { + val json = JsonObject() + json.add("food", food.toJson()) + json.addProperty("can_fly", canFly) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Bird { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Bird { + try { + val food = Food.fromJson(jsonObject.get("food").asString) + val canFly = jsonObject.get("can_fly").asBoolean + return Bird(food, canFly) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } + } + } + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Animal { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into one of type Animal", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Animal { + val errors = mutableListOf() + val asFish = try { + Fish.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val asBird = try { + Bird.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val result = arrayOf( + asFish, + asBird, + ).firstOrNull { it != null } + if (result == null) { + val message = "Unable to parse json into one of type \n" + "Animal\n" + + errors.joinToString("\n") { it.message.toString() } + throw JsonParseException(message) + } + return result + } + } + } + + public sealed class Situation { + public abstract fun toJson(): JsonElement + + public data class Marriage( + public val spouses: List, + ) : Situation() { + override fun toJson(): JsonElement { + val json = JsonObject() + val spousesArray = JsonArray(spouses.size) + spouses.forEach { spousesArray.add(it) } + json.add("spouses", spousesArray) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Marriage { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Marriage", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Marriage { + try { + val spouses = jsonObject.get("spouses").asJsonArray.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + return Marriage(spouses) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Marriage", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Marriage", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Marriage", + e + ) + } + } + } + } + + public data class Cotenancy( + public val roommates: List, + ) : Situation() { + override fun toJson(): JsonElement { + val json = JsonObject() + val roommatesArray = JsonArray(roommates.size) + roommates.forEach { roommatesArray.add(it) } + json.add("roommates", roommatesArray) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Cotenancy { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Cotenancy", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Cotenancy { + try { + val roommates = jsonObject.get("roommates").asJsonArray.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + return Cotenancy(roommates) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Cotenancy", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Cotenancy", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Cotenancy", + e + ) + } + } + } + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Situation { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into one of type Situation", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Situation { + val errors = mutableListOf() + val asMarriage = try { + Marriage.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val asCotenancy = try { + Cotenancy.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val result = arrayOf( + asMarriage, + asCotenancy, + ).firstOrNull { it != null } + if (result == null) { + val message = "Unable to parse json into one of type \n" + "Situation\n" + + errors.joinToString("\n") { it.message.toString() } + throw JsonParseException(message) + } + return result + } + } + } + + public enum class Water( + private val jsonValue: String, + ) { + SALT("salt"), + FRESH("fresh"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Water = values().first { + it.jsonValue == jsonString + } + } + } + + public enum class Food( + private val jsonValue: String, + ) { + FISH("fish"), + BIRD("bird"), + RODENT("rodent"), + INSECT("insect"), + FRUIT("fruit"), + SEEDS("seeds"), + POLLEN("pollen"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Food = values().first { + it.jsonValue == jsonString + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Jacket.kt b/buildSrc/src/test/kotlin/com/example/model/Jacket.kt new file mode 100644 index 0000000000..52e6f5a992 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Jacket.kt @@ -0,0 +1,83 @@ +package com.example.model + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Number +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Jacket( + public val size: Size = Size.SIZE_1, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + json.add("size", size.toJson()) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Jacket { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Jacket", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Jacket { + try { + val size = Size.fromJson(jsonObject.get("size").asString) + return Jacket(size) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Jacket", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Jacket", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Jacket", + e + ) + } + } + } + + public enum class Size( + private val jsonValue: Number, + ) { + SIZE_1(1), + SIZE_2(2), + SIZE_3(3), + SIZE_4(4), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Size = values().first { + it.jsonValue.toString() == jsonString + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Location.kt b/buildSrc/src/test/kotlin/com/example/model/Location.kt index 455e8e7f7f..5b295226d5 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Location.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Location.kt @@ -2,14 +2,67 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal class Location { - val planet: String = "earth" +public class Location { + public val planet: String = "earth" - fun toJson(): JsonElement { + public val solarSystem: String = "sol" + + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("planet", planet) + json.addProperty("solar_system", solarSystem) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Location { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Location", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Location { + try { + val planet = jsonObject.get("planet").asString + val solarSystem = jsonObject.get("solar_system").asString + check(planet == "earth") + check(solarSystem == "sol") + return Location() + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Location", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Location", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Location", + e + ) + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Message.kt b/buildSrc/src/test/kotlin/com/example/model/Message.kt new file mode 100644 index 0000000000..a3678b22ce --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Message.kt @@ -0,0 +1,110 @@ +package com.example.model + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Boolean +import kotlin.String +import kotlin.collections.ArrayList +import kotlin.collections.List +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Message( + public val destination: List, + public val origin: String, + public val subject: String? = null, + public val message: String? = null, + public var labels: List? = null, + public var read: Boolean? = null, + public var important: Boolean? = null, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + val destinationArray = JsonArray(destination.size) + destination.forEach { destinationArray.add(it) } + json.add("destination", destinationArray) + json.addProperty("origin", origin) + subject?.let { subjectNonNull -> + json.addProperty("subject", subjectNonNull) + } + message?.let { messageNonNull -> + json.addProperty("message", messageNonNull) + } + labels?.let { labelsNonNull -> + val labelsArray = JsonArray(labelsNonNull.size) + labelsNonNull.forEach { labelsArray.add(it) } + json.add("labels", labelsArray) + } + read?.let { readNonNull -> + json.addProperty("read", readNonNull) + } + important?.let { importantNonNull -> + json.addProperty("important", importantNonNull) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Message { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Message", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Message { + try { + val destination = jsonObject.get("destination").asJsonArray.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + val origin = jsonObject.get("origin").asString + val subject = jsonObject.get("subject")?.asString + val message = jsonObject.get("message")?.asString + val labels = jsonObject.get("labels")?.asJsonArray?.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + val read = jsonObject.get("read")?.asBoolean + val important = jsonObject.get("important")?.asBoolean + return Message(destination, origin, subject, message, labels, read, important) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Message", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Message", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Message", + e + ) + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Opus.kt b/buildSrc/src/test/kotlin/com/example/model/Opus.kt index 1b3739504a..c6fc6b9e8d 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Opus.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Opus.kt @@ -3,10 +3,18 @@ package com.example.model import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Long import kotlin.String +import kotlin.collections.ArrayList import kotlin.collections.List +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws /** * A musical opus. @@ -15,74 +23,168 @@ import kotlin.collections.List * @param artists The opus's artists. * @param duration The opus's duration in seconds */ -internal data class Opus( - val title: String? = null, - val composer: String? = null, - val artists: List? = null, - val duration: Long? = null +public data class Opus( + public val title: String? = null, + public val composer: String? = null, + public val artists: List? = null, + public val duration: Long? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (title != null) json.addProperty("title", title) - if (composer != null) json.addProperty("composer", composer) - if (artists != null) { - val artistsArray = JsonArray(artists.size) - artists.forEach { artistsArray.add(it.toJson()) } + title?.let { titleNonNull -> + json.addProperty("title", titleNonNull) + } + composer?.let { composerNonNull -> + json.addProperty("composer", composerNonNull) + } + artists?.let { artistsNonNull -> + val artistsArray = JsonArray(artistsNonNull.size) + artistsNonNull.forEach { artistsArray.add(it.toJson()) } json.add("artists", artistsArray) } - if (duration != null) json.addProperty("duration", duration) + duration?.let { durationNonNull -> + json.addProperty("duration", durationNonNull) + } return json } + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Opus { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Opus", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Opus { + try { + val title = jsonObject.get("title")?.asString + val composer = jsonObject.get("composer")?.asString + val artists = jsonObject.get("artists")?.asJsonArray?.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(Artist.fromJsonObject(it.asJsonObject)) + } + collection + } + val duration = jsonObject.get("duration")?.asLong + return Opus(title, composer, artists, duration) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Opus", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Opus", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Opus", + e + ) + } + } + } + /** * An artist and their role in an opus. * @param name The artist's name. * @param role The artist's role. */ - data class Artist( - val name: String? = null, - val role: Role? = null + public data class Artist( + public val name: String? = null, + public val role: Role? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (name != null) json.addProperty("name", name) - if (role != null) json.add("role", role.toJson()) + name?.let { nameNonNull -> + json.addProperty("name", nameNonNull) + } + role?.let { roleNonNull -> + json.add("role", roleNonNull.toJson()) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Artist { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Artist", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Artist { + try { + val name = jsonObject.get("name")?.asString + val role = jsonObject.get("role")?.asString?.let { + Role.fromJson(it) + } + return Artist(name, role) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Artist", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Artist", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Artist", + e + ) + } + } + } } /** * The artist's role. */ - enum class Role { - SINGER, - - GUITARIST, - - PIANIST, - - DRUMMER, - - BASSIST, - - VIOLINIST, - - DJ, - - VOCALS, + public enum class Role( + private val jsonValue: String, + ) { + SINGER("singer"), + GUITARIST("guitarist"), + PIANIST("pianist"), + DRUMMER("drummer"), + BASSIST("bassist"), + VIOLINIST("violinist"), + DJ("dj"), + VOCALS("vocals"), + OTHER("other"), + ; - OTHER; + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) - fun toJson(): JsonElement = when (this) { - SINGER -> JsonPrimitive("singer") - GUITARIST -> JsonPrimitive("guitarist") - PIANIST -> JsonPrimitive("pianist") - DRUMMER -> JsonPrimitive("drummer") - BASSIST -> JsonPrimitive("bassist") - VIOLINIST -> JsonPrimitive("violinist") - DJ -> JsonPrimitive("dj") - VOCALS -> JsonPrimitive("vocals") - OTHER -> JsonPrimitive("other") + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Role = values().first { + it.jsonValue == jsonString + } } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Order.kt b/buildSrc/src/test/kotlin/com/example/model/Order.kt new file mode 100644 index 0000000000..246e2a2fb9 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Order.kt @@ -0,0 +1,94 @@ +package com.example.model + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.String +import kotlin.collections.HashSet +import kotlin.collections.Set +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Order( + public val sizes: Set, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + val sizesArray = JsonArray(sizes.size) + sizes.forEach { sizesArray.add(it.toJson()) } + json.add("sizes", sizesArray) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Order { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Order", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Order { + try { + val sizes = jsonObject.get("sizes").asJsonArray.let { jsonArray -> + val collection = HashSet(jsonArray.size()) + jsonArray.forEach { + collection.add(Size.fromJson(it.asString)) + } + collection + } + return Order(sizes) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Order", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Order", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Order", + e + ) + } + } + } + + public enum class Size( + private val jsonValue: String, + ) { + X_SMALL("x small"), + SMALL("small"), + MEDIUM("medium"), + LARGE("large"), + X_LARGE("x large"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Size = values().first { + it.jsonValue == jsonString + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Paper.kt b/buildSrc/src/test/kotlin/com/example/model/Paper.kt new file mode 100644 index 0000000000..c9398e7798 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/Paper.kt @@ -0,0 +1,76 @@ +package com.example.model + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.String +import kotlin.collections.ArrayList +import kotlin.collections.List +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class Paper( + public val title: String, + public val author: List, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + json.addProperty("title", title) + val authorArray = JsonArray(author.size) + author.forEach { authorArray.add(it) } + json.add("author", authorArray) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Paper { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Paper { + try { + val title = jsonObject.get("title").asString + val author = jsonObject.get("author").asJsonArray.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + return Paper(title, author) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } + } + } +} diff --git a/buildSrc/src/test/kotlin/com/example/model/Person.kt b/buildSrc/src/test/kotlin/com/example/model/Person.kt index 023a5ea5d5..2bb9653474 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Person.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Person.kt @@ -2,19 +2,74 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Long import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Person( - val firstName: String? = null, - val lastName: String? = null, - val age: Long? = null +public data class Person( + public val firstName: String? = null, + public val lastName: String? = null, + public val age: Long? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (firstName != null) json.addProperty("firstName", firstName) - if (lastName != null) json.addProperty("lastName", lastName) - if (age != null) json.addProperty("age", age) + firstName?.let { firstNameNonNull -> + json.addProperty("firstName", firstNameNonNull) + } + lastName?.let { lastNameNonNull -> + json.addProperty("lastName", lastNameNonNull) + } + age?.let { ageNonNull -> + json.addProperty("age", ageNonNull) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Person { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Person", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Person { + try { + val firstName = jsonObject.get("firstName")?.asString + val lastName = jsonObject.get("lastName")?.asString + val age = jsonObject.get("age")?.asLong + return Person(firstName, lastName, age) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Person", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Person", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Person", + e + ) + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Product.kt b/buildSrc/src/test/kotlin/com/example/model/Product.kt index 07368bc369..b28eadb7c8 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Product.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Product.kt @@ -2,20 +2,69 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject -import kotlin.Double +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Long +import kotlin.Number import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Product( - val productId: Long, - val productName: String, - val price: Double +public data class Product( + public val productId: Long, + public val productName: String, + public val price: Number, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("productId", productId) json.addProperty("productName", productName) json.addProperty("price", price) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Product { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Product", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Product { + try { + val productId = jsonObject.get("productId").asLong + val productName = jsonObject.get("productName").asString + val price = jsonObject.get("price").asNumber + return Product(productId, productName, price) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Product", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Product", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Product", + e + ) + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Shipping.kt b/buildSrc/src/test/kotlin/com/example/model/Shipping.kt index c1d8db3a4b..3a3f55f285 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Shipping.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Shipping.kt @@ -2,30 +2,122 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Shipping( - val item: String, - val destination: Address +public data class Shipping( + public val item: String, + public val destination: Address, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("item", item) json.add("destination", destination.toJson()) return json } - data class Address( - val streetAddress: String, - val city: String, - val state: String + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Shipping { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Shipping", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Shipping { + try { + val item = jsonObject.get("item").asString + val destination = jsonObject.get("destination").asJsonObject.let { + Address.fromJsonObject(it) + } + return Shipping(item, destination) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Shipping", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Shipping", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Shipping", + e + ) + } + } + } + + public data class Address( + public val streetAddress: String, + public val city: String, + public val state: String, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("street_address", streetAddress) json.addProperty("city", city) json.addProperty("state", state) return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Address { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Address { + try { + val streetAddress = jsonObject.get("street_address").asString + val city = jsonObject.get("city").asString + val state = jsonObject.get("state").asString + return Address(streetAddress, city, state) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Address", + e + ) + } + } + } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Style.kt b/buildSrc/src/test/kotlin/com/example/model/Style.kt index 04c6be061c..266da48dd7 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Style.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Style.kt @@ -1,32 +1,97 @@ package com.example.model import com.google.gson.JsonElement +import com.google.gson.JsonNull import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Style( - val color: Color +public data class Style( + public val color: Color, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.add("color", color.toJson()) return json } - enum class Color { - RED, + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Style { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Style", + e + ) + } + } - AMBER, + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Style { + try { + val jsonColor = jsonObject.get("color") + val color = if (jsonColor is JsonNull || jsonColor == null) { + Color.fromJson(null) + } else { + Color.fromJson(jsonColor.asString) + } + return Style(color) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Style", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Style", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Style", + e + ) + } + } + } - GREEN, + public enum class Color( + private val jsonValue: String?, + ) { + RED("red"), + AMBER("amber"), + GREEN("green"), + DARK_BLUE("dark_blue"), + LIME_GREEN("lime green"), + SUNBURST_YELLOW("sunburst-yellow"), + COLOR_NULL(null), + ; - DARK_BLUE; + public fun toJson(): JsonElement { + if (jsonValue == null) { + return JsonNull.INSTANCE + } else { + return JsonPrimitive(jsonValue) + } + } - fun toJson(): JsonElement = when (this) { - RED -> JsonPrimitive("red") - AMBER -> JsonPrimitive("amber") - GREEN -> JsonPrimitive("green") - DARK_BLUE -> JsonPrimitive("dark_blue") + public companion object { + @JvmStatic + public fun fromJson(jsonString: String?): Color = values().first { + it.jsonValue == jsonString + } } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/User.kt b/buildSrc/src/test/kotlin/com/example/model/User.kt index 6c3f314044..7139f2f4a7 100644 --- a/buildSrc/src/test/kotlin/com/example/model/User.kt +++ b/buildSrc/src/test/kotlin/com/example/model/User.kt @@ -2,34 +2,93 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class User( - val username: String, - val host: String, - val firstname: String? = null, - val lastname: String, - val contactType: ContactType +public data class User( + public val username: String, + public val host: String, + public val firstname: String? = null, + public val lastname: String, + public val contactType: ContactType, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("username", username) json.addProperty("host", host) - if (firstname != null) json.addProperty("firstname", firstname) + firstname?.let { firstnameNonNull -> + json.addProperty("firstname", firstnameNonNull) + } json.addProperty("lastname", lastname) json.add("contact_type", contactType.toJson()) return json } - enum class ContactType { - PERSONAL, + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): User { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): User { + try { + val username = jsonObject.get("username").asString + val host = jsonObject.get("host").asString + val firstname = jsonObject.get("firstname")?.asString + val lastname = jsonObject.get("lastname").asString + val contactType = ContactType.fromJson(jsonObject.get("contact_type").asString) + return User(username, host, firstname, lastname, contactType) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type User", + e + ) + } + } + } + + public enum class ContactType( + private val jsonValue: String, + ) { + PERSONAL("personal"), + PROFESSIONAL("professional"), + ; - PROFESSIONAL; + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) - fun toJson(): JsonElement = when (this) { - PERSONAL -> JsonPrimitive("personal") - PROFESSIONAL -> JsonPrimitive("professional") + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): ContactType = values().first { + it.jsonValue == jsonString + } } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/UserMerged.kt b/buildSrc/src/test/kotlin/com/example/model/UserMerged.kt index 1bad6d03ec..ad6d0e8077 100644 --- a/buildSrc/src/test/kotlin/com/example/model/UserMerged.kt +++ b/buildSrc/src/test/kotlin/com/example/model/UserMerged.kt @@ -2,34 +2,140 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class UserMerged( - val email: String? = null, - val phone: String? = null, - val info: Info? = null, - val firstname: String? = null, - val lastname: String +public data class UserMerged( + public val email: String? = null, + public val phone: String? = null, + public val info: Info? = null, + public val firstname: String? = null, + public val lastname: String, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (email != null) json.addProperty("email", email) - if (phone != null) json.addProperty("phone", phone) - if (info != null) json.add("info", info.toJson()) - if (firstname != null) json.addProperty("firstname", firstname) + email?.let { emailNonNull -> + json.addProperty("email", emailNonNull) + } + phone?.let { phoneNonNull -> + json.addProperty("phone", phoneNonNull) + } + info?.let { infoNonNull -> + json.add("info", infoNonNull.toJson()) + } + firstname?.let { firstnameNonNull -> + json.addProperty("firstname", firstnameNonNull) + } json.addProperty("lastname", lastname) return json } - data class Info( - val notes: String? = null, - val source: String? = null + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): UserMerged { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type UserMerged", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): UserMerged { + try { + val email = jsonObject.get("email")?.asString + val phone = jsonObject.get("phone")?.asString + val info = jsonObject.get("info")?.asJsonObject?.let { + Info.fromJsonObject(it) + } + val firstname = jsonObject.get("firstname")?.asString + val lastname = jsonObject.get("lastname").asString + return UserMerged(email, phone, info, firstname, lastname) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type UserMerged", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type UserMerged", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type UserMerged", + e + ) + } + } + } + + public data class Info( + public val notes: String? = null, + public val source: String? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - if (notes != null) json.addProperty("notes", notes) - if (source != null) json.addProperty("source", source) + notes?.let { notesNonNull -> + json.addProperty("notes", notesNonNull) + } + source?.let { sourceNonNull -> + json.addProperty("source", sourceNonNull) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Info { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Info", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Info { + try { + val notes = jsonObject.get("notes")?.asString + val source = jsonObject.get("source")?.asString + return Info(notes, source) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Info", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Info", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Info", + e + ) + } + } + } } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Version.kt b/buildSrc/src/test/kotlin/com/example/model/Version.kt index cbce305b21..19272bd21d 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Version.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Version.kt @@ -2,18 +2,197 @@ package com.example.model import com.google.gson.JsonElement import com.google.gson.JsonObject -import kotlin.Double +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.Long +import kotlin.Number +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal class Version { - val version: Long = 42L +public data class Version( + public val id: Id, + public val date: Date? = null, +) { + public val major: Long = 42L - val delta: Double = 3.1415 + public val delta: Number = 3.1415 - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() - json.addProperty("version", version) + json.addProperty("major", major) json.addProperty("delta", delta) + json.add("id", id.toJson()) + date?.let { dateNonNull -> + json.add("date", dateNonNull.toJson()) + } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Version { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Version", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Version { + try { + val major = jsonObject.get("major").asLong + val delta = jsonObject.get("delta")?.asNumber + val id = jsonObject.get("id").asJsonObject.let { + Id.fromJsonObject(it) + } + val date = jsonObject.get("date")?.asJsonObject?.let { + Date.fromJsonObject(it) + } + check(major == 42.0.toLong()) + if (delta != null) { + check(delta.toDouble() == 3.1415) + } + return Version(id, date) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Version", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Version", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Version", + e + ) + } + } + } + + public class Id { + public val serialNumber: Number = 12112.0 + + public fun toJson(): JsonElement { + val json = JsonObject() + json.addProperty("serialNumber", serialNumber) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Id { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Id", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Id { + try { + val serialNumber = jsonObject.get("serialNumber")?.asNumber + if (serialNumber != null) { + check(serialNumber.toDouble() == 12112.0) + } + return Id() + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Id", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Id", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Id", + e + ) + } + } + } + } + + public class Date { + public val year: Long = 2021L + + public val month: Long = 3L + + public fun toJson(): JsonElement { + val json = JsonObject() + json.addProperty("year", year) + json.addProperty("month", month) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Date { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Date { + try { + val year = jsonObject.get("year")?.asLong + val month = jsonObject.get("month")?.asLong + if (year != null) { + check(year == 2021.0.toLong()) + } + if (month != null) { + check(month == 3.0.toLong()) + } + return Date() + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Date", + e + ) + } + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/Video.kt b/buildSrc/src/test/kotlin/com/example/model/Video.kt index d6c307bd72..e672f532a9 100644 --- a/buildSrc/src/test/kotlin/com/example/model/Video.kt +++ b/buildSrc/src/test/kotlin/com/example/model/Video.kt @@ -3,27 +3,89 @@ package com.example.model import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException import kotlin.String +import kotlin.collections.HashSet import kotlin.collections.Set +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws -internal data class Video( - val title: String, - val tags: Set? = null, - val links: Set? = null +public data class Video( + public val title: String, + public val tags: Set? = null, + public val links: Set? = null, ) { - fun toJson(): JsonElement { + public fun toJson(): JsonElement { val json = JsonObject() json.addProperty("title", title) - if (tags != null) { - val tagsArray = JsonArray(tags.size) - tags.forEach { tagsArray.add(it) } + tags?.let { tagsNonNull -> + val tagsArray = JsonArray(tagsNonNull.size) + tagsNonNull.forEach { tagsArray.add(it) } json.add("tags", tagsArray) } - if (links != null) { - val linksArray = JsonArray(links.size) - links.forEach { linksArray.add(it) } + links?.let { linksNonNull -> + val linksArray = JsonArray(linksNonNull.size) + linksNonNull.forEach { linksArray.add(it) } json.add("links", linksArray) } return json } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Video { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Video", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Video { + try { + val title = jsonObject.get("title").asString + val tags = jsonObject.get("tags")?.asJsonArray?.let { jsonArray -> + val collection = HashSet(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + val links = jsonObject.get("links")?.asJsonArray?.let { jsonArray -> + val collection = HashSet(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + return Video(title, tags, links) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Video", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Video", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Video", + e + ) + } + } + } } diff --git a/buildSrc/src/test/kotlin/com/example/model/WeirdCombo.kt b/buildSrc/src/test/kotlin/com/example/model/WeirdCombo.kt new file mode 100644 index 0000000000..aa5c58d7c9 --- /dev/null +++ b/buildSrc/src/test/kotlin/com/example/model/WeirdCombo.kt @@ -0,0 +1,335 @@ +package com.example.model + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Boolean +import kotlin.Long +import kotlin.String +import kotlin.collections.ArrayList +import kotlin.collections.List +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +public data class WeirdCombo( + public val anything: Anything? = null, +) { + public fun toJson(): JsonElement { + val json = JsonObject() + anything?.let { anythingNonNull -> + json.add("anything", anythingNonNull.toJson()) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): WeirdCombo { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type WeirdCombo", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): WeirdCombo { + try { + val anything = jsonObject.get("anything")?.asJsonObject?.let { + Anything.fromJsonObject(it) + } + return WeirdCombo(anything) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type WeirdCombo", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type WeirdCombo", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type WeirdCombo", + e + ) + } + } + } + + public sealed class Anything { + public abstract fun toJson(): JsonElement + + public data class Fish( + public val water: Water, + public val size: Long? = null, + ) : Anything() { + override fun toJson(): JsonElement { + val json = JsonObject() + json.add("water", water.toJson()) + size?.let { sizeNonNull -> + json.addProperty("size", sizeNonNull) + } + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Fish { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Fish { + try { + val water = Water.fromJson(jsonObject.get("water").asString) + val size = jsonObject.get("size")?.asLong + return Fish(water, size) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Fish", + e + ) + } + } + } + } + + public data class Bird( + public val food: Food, + public val canFly: Boolean, + ) : Anything() { + override fun toJson(): JsonElement { + val json = JsonObject() + json.add("food", food.toJson()) + json.addProperty("can_fly", canFly) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Bird { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Bird { + try { + val food = Food.fromJson(jsonObject.get("food").asString) + val canFly = jsonObject.get("can_fly").asBoolean + return Bird(food, canFly) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Bird", + e + ) + } + } + } + } + + public data class Paper( + public val title: String, + public val author: List, + ) : Anything() { + override fun toJson(): JsonElement { + val json = JsonObject() + json.addProperty("title", title) + val authorArray = JsonArray(author.size) + author.forEach { authorArray.add(it) } + json.add("author", authorArray) + return json + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Paper { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Paper { + try { + val title = jsonObject.get("title").asString + val author = jsonObject.get("author").asJsonArray.let { jsonArray -> + val collection = ArrayList(jsonArray.size()) + jsonArray.forEach { + collection.add(it.asString) + } + collection + } + return Paper(title, author) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } catch (e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type Paper", + e + ) + } + } + } + } + + public companion object { + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJson(jsonString: String): Anything { + try { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into one of type Anything", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + public fun fromJsonObject(jsonObject: JsonObject): Anything { + val errors = mutableListOf() + val asFish = try { + Fish.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val asBird = try { + Bird.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val asPaper = try { + Paper.fromJsonObject(jsonObject) + } catch (e: JsonParseException) { + errors.add(e) + null + } + val result = arrayOf( + asFish, + asBird, + asPaper, + ).firstOrNull { it != null } + if (result == null) { + val message = "Unable to parse json into one of type \n" + "Anything\n" + + errors.joinToString("\n") { it.message.toString() } + throw JsonParseException(message) + } + return result + } + } + } + + public enum class Water( + private val jsonValue: String, + ) { + SALT("salt"), + FRESH("fresh"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Water = values().first { + it.jsonValue == jsonString + } + } + } + + public enum class Food( + private val jsonValue: String, + ) { + FISH("fish"), + BIRD("bird"), + RODENT("rodent"), + INSECT("insect"), + FRUIT("fruit"), + SEEDS("seeds"), + POLLEN("pollen"), + ; + + public fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + public companion object { + @JvmStatic + public fun fromJson(jsonString: String): Food = values().first { + it.jsonValue == jsonString + } + } + } +} diff --git a/buildSrc/src/test/resources/input/additional_props.json b/buildSrc/src/test/resources/input/additional_props.json new file mode 100644 index 0000000000..1e08b63ea0 --- /dev/null +++ b/buildSrc/src/test/resources/input/additional_props.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Comment", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "ratings": { + "type": "object", + "properties": { + "global": { + "type": "integer" + } + }, + "additionalProperties": { + "type": "integer", + "readOnly": true + }, + "required": [ + "global" + ] + }, + "flags": { + "additionalProperties": { + "type": "boolean" + } + }, + "tags": { + "additionalProperties": { + "type": "string", + "readOnly": false + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/additional_props_any.json b/buildSrc/src/test/resources/input/additional_props_any.json new file mode 100644 index 0000000000..7107dc254a --- /dev/null +++ b/buildSrc/src/test/resources/input/additional_props_any.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Company", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "ratings": { + "type": "object", + "properties": { + "global": { + "type": "integer" + } + }, + "additionalProperties": { + "type": "integer" + }, + "required": [ "global"] + }, + "information": { + "properties": { + "date":{ + "type": "integer" + }, + "priority": { + "type": "integer" + } + }, + "additionalProperties": { + "type": "object" + } + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/additional_props_merged.json b/buildSrc/src/test/resources/input/additional_props_merged.json new file mode 100644 index 0000000000..d0fbddf660 --- /dev/null +++ b/buildSrc/src/test/resources/input/additional_props_merged.json @@ -0,0 +1,61 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/contact" + }, + { + "$ref": "#/definitions/identity" + } + ], + "definitions": { + "contact": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "info": { + "type": "object", + "additionalProperties": true, + "properties": { + "notes": { + "type": "string" + } + } + } + } + }, + "identity": { + "type": "object", + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "email": { + "type": "string" + }, + "info": { + "type": "object", + "additionalProperties": true, + "properties": { + "source": { + "type": "string" + } + } + } + }, + "required": [ + "lastname" + ] + } + } +} diff --git a/buildSrc/src/test/resources/input/additional_props_single_merge.json b/buildSrc/src/test/resources/input/additional_props_single_merge.json new file mode 100644 index 0000000000..3edca675c5 --- /dev/null +++ b/buildSrc/src/test/resources/input/additional_props_single_merge.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/contact" + }, + { + "$ref": "#/definitions/identity" + } + ], + "definitions": { + "contact": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "info": { + "type": "object", + "additionalProperties": true, + "properties": { + "notes": { + "type": "string" + } + } + } + } + }, + "identity": { + "type": "object", + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "email": { + "type": "string" + }, + "info": { + "type": "object", + "properties": { + "source": { + "type": "string" + } + } + } + }, + "required": [ + "lastname" + ] + } + } +} diff --git a/buildSrc/src/test/resources/input/constant.json b/buildSrc/src/test/resources/input/constant.json index 5a615f7ae7..849748d417 100644 --- a/buildSrc/src/test/resources/input/constant.json +++ b/buildSrc/src/test/resources/input/constant.json @@ -6,7 +6,11 @@ "planet": { "type" : "string", "const": "earth" + }, + "solar_system": { + "type" : "string", + "const": "sol" } }, - "required": [ "planet"] + "required": [ "planet", "solar_system"] } \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/constant_number.json b/buildSrc/src/test/resources/input/constant_number.json index a8d4e4241e..52a08b0e70 100644 --- a/buildSrc/src/test/resources/input/constant_number.json +++ b/buildSrc/src/test/resources/input/constant_number.json @@ -3,14 +3,39 @@ "title": "Version", "type": "object", "properties": { - "version": { - "type" : "integer", + "major": { + "type": "integer", "const": 42 }, "delta": { - "type" : "number", + "type": "number", "const": 3.1415 + }, + "id": { + "type": "object", + "properties": { + "serialNumber": { + "type": "number", + "const": 12112.0 + } + } + }, + "date": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "const": 2021 + }, + "month": { + "type": "integer", + "const": 3 + } + } } }, - "required": [ "version"] + "required": [ + "major", + "id" + ] } \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/defaults_with_optionals.json b/buildSrc/src/test/resources/input/defaults_with_optionals.json new file mode 100644 index 0000000000..5af700a213 --- /dev/null +++ b/buildSrc/src/test/resources/input/defaults_with_optionals.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Bike", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "default": 1 + }, + "productName": { + "type": "string" + }, + "type": { + "type": "string", + "default": "road" + }, + "price": { + "type": "number", + "default": 55.5 + }, + "frameMaterial": { + "type": "string", + "enum": [ + "carbon", + "light_aluminium", + "iron" + ], + "default": "light_aluminium" + }, + "inStock": { + "type": "boolean", + "default": true + }, + "color": { + "type": "string", + "enum": [ + "red", + "amber", + "green", + "dark_blue", + "lime green", + "sunburst-yellow" + ], + "default": "lime green" + } + }, + "required": [ + "productId", + "productName", + "price", + "inStock", + "color" + ] +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/enum.json b/buildSrc/src/test/resources/input/enum.json index f4f244e345..7bfef40f58 100644 --- a/buildSrc/src/test/resources/input/enum.json +++ b/buildSrc/src/test/resources/input/enum.json @@ -4,8 +4,7 @@ "type": "object", "properties": { "color": { - "type": "string", - "enum": ["red", "amber", "green", "dark_blue"] + "enum": ["red", "amber", "green", "dark_blue", "lime green", "sunburst-yellow", null] } }, "required": [ "color"] diff --git a/buildSrc/src/test/resources/input/enum_array.json b/buildSrc/src/test/resources/input/enum_array.json new file mode 100644 index 0000000000..d9ac3490c1 --- /dev/null +++ b/buildSrc/src/test/resources/input/enum_array.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Order", + "type": "object", + "properties": { + "sizes": { + "type": "array", + "items": { + "type": "string", + "enum": ["x small", "small", "medium", "large", "x large"] + }, + "uniqueItems": true + } + }, + "required": [ + "sizes" + ] +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/enum_number.json b/buildSrc/src/test/resources/input/enum_number.json new file mode 100644 index 0000000000..9f6a11427f --- /dev/null +++ b/buildSrc/src/test/resources/input/enum_number.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Jacket", + "type": "object", + "properties": { + "size": { + "type": "number", + "enum": [ + 1, + 2, + 3, + 4 + ], + "default": 1.0 + } + }, + "required": [ + "size" + ] +} diff --git a/buildSrc/src/test/resources/input/external_description_complex_path.json b/buildSrc/src/test/resources/input/external_description_complex_path.json new file mode 100644 index 0000000000..e358a5b105 --- /dev/null +++ b/buildSrc/src/test/resources/input/external_description_complex_path.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Employee", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "contact": { + "$ref": "subfolder/external_description_proxy.json#/definitions/contact" + } + }, + "required": [ + "item", + "destination" + ] +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/external_nested_description_properties.json b/buildSrc/src/test/resources/input/external_nested_description_properties.json new file mode 100644 index 0000000000..138a9c9f60 --- /dev/null +++ b/buildSrc/src/test/resources/input/external_nested_description_properties.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Shipping", + "type": "object", + "properties": { + "item": { + "type": "string" + }, + "destination": { + "$ref": "properties.json#/properties/billing_address" + } + }, + "required": [ + "item", + "destination" + ] +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/one_of.json b/buildSrc/src/test/resources/input/one_of.json new file mode 100644 index 0000000000..f56faa05cf --- /dev/null +++ b/buildSrc/src/test/resources/input/one_of.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Animal", + "type": "object", + "description": "A representation of the animal kingdom", + "oneOf": [ + { + "type": "object", + "title": "Fish", + "properties": { + "water": { + "type": "string", + "enum": ["salt", "fresh"] + }, + "size": { + "type": "integer" + } + }, + "required": ["water"] + }, + { + "type": "object", + "title": "Bird", + "properties": { + "food": { + "type": "string", + "enum": ["fish", "bird", "rodent", "insect", "fruit", "seeds", "pollen"] + }, + "can_fly": { + "type": "boolean" + } + }, + "required": ["food", "can_fly"] + } + ] +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/one_of_complex.json b/buildSrc/src/test/resources/input/one_of_complex.json new file mode 100644 index 0000000000..5d5758b0cf --- /dev/null +++ b/buildSrc/src/test/resources/input/one_of_complex.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Paper", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "author": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "title", "author" + ] +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/one_of_nested.json b/buildSrc/src/test/resources/input/one_of_nested.json new file mode 100644 index 0000000000..fc4d56190f --- /dev/null +++ b/buildSrc/src/test/resources/input/one_of_nested.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "WeirdCombo", + "type": "object", + "properties": { + "anything": { + "oneOf": [ + { + "$ref": "one_of.json" + }, + { + "$ref": "one_of_complex.json" + } + ] + } + } +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/one_of_ref.json b/buildSrc/src/test/resources/input/one_of_ref.json new file mode 100644 index 0000000000..47426d4092 --- /dev/null +++ b/buildSrc/src/test/resources/input/one_of_ref.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Household", + "type": "object", + "properties": { + "pets": { + "type": "array", + "items": { + "$ref": "one_of.json" + }, + "readOnly": true + }, + "situation": { + "oneOf": [ + { + "title": "Marriage", + "properties": { + "spouses": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "spouses" + ] + }, + { + "title": "Cotenancy", + "properties": { + "roommates": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "roommates" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/properties.json b/buildSrc/src/test/resources/input/properties.json new file mode 100644 index 0000000000..56aa4b2653 --- /dev/null +++ b/buildSrc/src/test/resources/input/properties.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Customer", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "billing_address": { "$ref": "#/definitions/address" }, + "shipping_address": { "$ref": "#/definitions/address" } + }, + "definitions": { + "address": { + "type": "object", + "properties": { + "street_address": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + } + }, + "required": [ + "street_address", + "city", + "state" + ] + } + } +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/read_only.json b/buildSrc/src/test/resources/input/read_only.json new file mode 100644 index 0000000000..f5cc43aa53 --- /dev/null +++ b/buildSrc/src/test/resources/input/read_only.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Message", + "type": "object", + "properties": { + "destination": { + "type": "array", + "items": { + "type": "string" + }, + "readOnly": true + }, + "origin": { + "type": "string", + "readOnly": true + }, + "subject": { + "type": "string", + "readOnly": true + }, + "message": { + "type": "string", + "readOnly": true + }, + "labels": { + "type": "array", + "items": { + "type": "string" + }, + "readOnly": false + }, + "read": { + "type": "boolean", + "readOnly": false + }, + "important": { + "type": "boolean", + "readOnly": false + } + }, + "required": [ + "destination", + "origin" + ] +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/root_schema_reference.json b/buildSrc/src/test/resources/input/root_schema_reference.json new file mode 100644 index 0000000000..00141cce11 --- /dev/null +++ b/buildSrc/src/test/resources/input/root_schema_reference.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "$id": "root-schema-reference", + "title": "Country", + "properties": { + "name": { + "type": "string" + }, + "continent": { + "type": "string" + }, + "population": { + "type": "integer" + } + } +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/root_schema_with_no_type.json b/buildSrc/src/test/resources/input/root_schema_with_no_type.json new file mode 100644 index 0000000000..c97b1cbb6f --- /dev/null +++ b/buildSrc/src/test/resources/input/root_schema_with_no_type.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "root-schema-with-no-type", + "$ref": "root_schema_reference.json" +} \ No newline at end of file diff --git a/buildSrc/src/test/resources/input/subfolder/external_description_proxy.json b/buildSrc/src/test/resources/input/subfolder/external_description_proxy.json new file mode 100644 index 0000000000..a4e30e2e4f --- /dev/null +++ b/buildSrc/src/test/resources/input/subfolder/external_description_proxy.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "contact": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "address" : { + "$ref": "../definition.json#/definitions/address" + } + }, + "required": [ + "phone", + "address" + ] + } + } +} \ No newline at end of file diff --git a/ci/Dockerfile.gitlab b/ci/Dockerfile.gitlab new file mode 100644 index 0000000000..40b80b3887 --- /dev/null +++ b/ci/Dockerfile.gitlab @@ -0,0 +1,111 @@ +# This base image is based on a GBI image +FROM registry.ddbuild.io/images/docker:24.0.4-jammy + +ENV DEBIAN_FRONTEND=noninteractive + +# Set timezone to UTC by default +RUN ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime + +# keep in sync with JAVA_HOME path below +RUN apt-get update \ + && apt-get -y install openjdk-17-jdk \ + && rm -rf /var/lib/apt/lists/* + +RUN set -x \ + && apt-get update \ + && apt-get -y upgrade \ + && apt-get -y install --no-install-recommends \ + curl \ + git \ + unzip \ + wget \ + openssh-client \ + expect \ + python3-distutils \ + python3-apt \ + && apt-get -y clean \ + && rm -rf /var/lib/apt/lists/* + +ENV GRADLE_VERSION 8.11.1 +ENV ANDROID_COMPILE_SDK 36 +ENV ANDROID_BUILD_TOOLS 36.0.0 +ENV ANDROID_SDK_TOOLS 11076708 +ENV NDK_VERSION 28.0.13004108 +ENV CMAKE_VERSION 3.22.1 +ENV DD_TRACER_VERSION 1.54.0 +# requires build with BuildKit to be available https://docs.docker.com/build/building/variables/#multi-platform-build-arguments +ARG TARGETARCH +ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-${TARGETARCH} + +RUN apt update && apt install -y python3 + +# Install pip for aws +RUN set -x \ + && curl -OL https://bootstrap.pypa.io/pip/3.8/get-pip.py \ + && python3 get-pip.py \ + && rm get-pip.py + +RUN python3 --version + +RUN set -x \ + && pip install awscli + +# Gradle +RUN \ + cd /usr/local && \ + curl -L https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip -o gradle-${GRADLE_VERSION}-bin.zip && \ + unzip gradle-${GRADLE_VERSION}-bin.zip && \ + rm gradle-${GRADLE_VERSION}-bin.zip + +# Workaround for +# Warning: File /root/.android/repositories.cfg could not be loaded. +RUN mkdir /root/.android \ + && touch /root/.android/repositories.cfg + + +# Android SDK +RUN \ + wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip && \ + mkdir -p android-sdk-linux/cmdline-tools && \ + unzip -d android-sdk-linux/cmdline-tools android-sdk.zip && \ + mv android-sdk-linux/cmdline-tools/cmdline-tools android-sdk-linux/cmdline-tools/latest && \ + echo y | android-sdk-linux/cmdline-tools/latest/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null && \ + echo y | android-sdk-linux/cmdline-tools/latest/bin/sdkmanager "platform-tools" >/dev/null && \ + echo y | android-sdk-linux/cmdline-tools/latest/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null && \ + echo y | android-sdk-linux/cmdline-tools/latest/bin/sdkmanager --install "ndk;${NDK_VERSION}" >/dev/null && \ + echo y | android-sdk-linux/cmdline-tools/latest/bin/sdkmanager --install "cmake;${CMAKE_VERSION}" >/dev/null && \ + (yes || true) | android-sdk-linux/cmdline-tools/latest/bin/sdkmanager --licenses + +RUN set -x \ + && curl -OL https://s3.amazonaws.com/dd-package-public/dd-package.deb && dpkg -i dd-package.deb && rm dd-package.deb \ + && apt-get update \ + && apt-get -y clean \ + && rm -rf /var/lib/apt/lists/* + +ENV ANDROID_SDK_ROOT $PWD/android-sdk-linux +ENV ANDROID_HOME $PWD/android-sdk-linux +ENV GRADLE_HOME /usr/local/gradle-${GRADLE_VERSION} +ENV ANDROID_NDK $ANDROID_SDK_ROOT/ndk/${NDK_VERSION} +ENV PATH $PATH:$GRADLE_HOME/bin +ENV PATH $PATH:$ANDROID_HOME/platform-tools +ENV PATH $PATH:$ANDROID_SDK_ROOT/build-tools/${ANDROID_BUILD_TOOLS}:$ANDROID_NDK + +# Install Node +ENV NODENV_VERSION 22.19.0 +ENV NODENV_ROOT /root/.nodenv +ENV PATH "$NODENV_ROOT/shims:$NODENV_ROOT/bin:$PATH" +RUN set -x \ + && curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-installer | bash \ + && nodenv install $NODENV_VERSION \ + && nodenv rehash + +# Install Datadog CI +RUN npm install -g npm@9.6.5 +RUN npm install -g @datadog/datadog-ci + +# Install Datadog Java tracer +ENV DD_TRACER_FOLDER $PWD/dd-java-agent +RUN mkdir -p $DD_TRACER_FOLDER +RUN wget -O $DD_TRACER_FOLDER/dd-java-agent.jar https://repo1.maven.org/maven2/com/datadoghq/dd-java-agent/$DD_TRACER_VERSION/dd-java-agent-$DD_TRACER_VERSION.jar + +COPY --from=registry.ddbuild.io/dd-octo-sts:v1.8.2 /usr/local/bin/dd-octo-sts /usr/local/bin/dd-octo-sts diff --git a/ci/pipelines/check-release-pipeline.yml b/ci/pipelines/check-release-pipeline.yml new file mode 100644 index 0000000000..7ef2035b50 --- /dev/null +++ b/ci/pipelines/check-release-pipeline.yml @@ -0,0 +1,26 @@ +include: + - 'https://gitlab-templates.ddbuild.io/slack-notifier/v1/template.yml' + +stages: + - check-release + - notify + +check-release:is-published: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: check-release + id_tokens: + DDOCTOSTS_ID_TOKEN: + aud: dd-octo-sts + script: + - export GITHUB_TOKEN=$(dd-octo-sts token --scope DataDog/dd-sdk-android --policy self.gitlab.read) + - bash ./ci/scripts/check_latest_release_is_published.sh + +notify:report-failure-to-slack: + extends: .slack-notifier-base + stage: notify + when: on_failure + script: + - BUILD_URL="$CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID" + - 'MESSAGE_TEXT=":status_alert: Some release artifacts were not published to maven. $BUILD_URL"' + - postmessage "#mobile-sdk-ops" "$MESSAGE_TEXT" diff --git a/ci/pipelines/default-pipeline.yml b/ci/pipelines/default-pipeline.yml new file mode 100644 index 0000000000..02d09e782b --- /dev/null +++ b/ci/pipelines/default-pipeline.yml @@ -0,0 +1,1091 @@ +include: + - 'https://gitlab-templates.ddbuild.io/slack-notifier/v1/template.yml' + +# SETUP + +stages: + - ci-image + - security + - analysis + - test # TODO RUM-1622 cleanup eventually + - test-pyramid + - publish + - notify + +.snippets: + # macOS AMI will already have cmdline-tools installed + install-android-api-components: + - echo y | ~/android_sdk/cmdline-tools/latest/bin/sdkmanager --install "emulator" + - echo y | ~/android_sdk/cmdline-tools/latest/bin/sdkmanager --install "platform-tools" + - echo y | ~/android_sdk/cmdline-tools/latest/bin/sdkmanager --install "$ANDROID_PLATFORM" + - echo y | ~/android_sdk/cmdline-tools/latest/bin/sdkmanager --install "$ANDROID_BUILD_TOOLS" + - echo y | ~/android_sdk/cmdline-tools/latest/bin/sdkmanager --install "$ANDROID_EMULATOR_IMAGE" + - yes | ~/android_sdk/cmdline-tools/latest/bin/sdkmanager --licenses || true + - echo "no" | ~/android_sdk/cmdline-tools/latest/bin/avdmanager --verbose create avd --force --name "$EMULATOR_NAME" --package "$ANDROID_EMULATOR_IMAGE" + run-legacy-integration-instrumented: + - set +e + - exit_code=0 + - $ANDROID_HOME/emulator/emulator -avd "$EMULATOR_NAME" -grpc-use-jwt -no-snapstorage -no-audio -no-window -no-boot-anim -verbose -qemu -machine virt & + - GRADLE_OPTS="-Xmx3072m" ./gradlew :instrumented:integration:connectedDebugAndroidTest --stacktrace --no-daemon $( (( $ANDROID_API <= 23 )) && echo "-Puse-api21-java-backport -Puse-desugaring" ) || exit_code=$? + - $ANDROID_HOME/platform-tools/adb emu kill + - if [[ "$exit_code" -ne 0 ]]; then exit 1; fi + - exit 0 + run-core-it-instrumented: + - set +e + - exit_code=0 + - $ANDROID_HOME/emulator/emulator -avd "$EMULATOR_NAME" -grpc-use-jwt -no-snapstorage -no-audio -no-window -no-boot-anim -verbose -qemu -machine virt & + - GRADLE_OPTS="-Xmx3072m" ./gradlew :reliability:core-it:connectedDebugAndroidTest --stacktrace --no-daemon $( (( $ANDROID_API <= 23 )) && echo "-Puse-api21-java-backport -Puse-desugaring" ) || exit_code=$? + - $ANDROID_HOME/platform-tools/adb emu kill + - if [[ "$exit_code" -ne 0 ]]; then exit 1; fi + - exit 0 + set-publishing-credentials: + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties + - export GPG_PRIVATE_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.gpg_private_key --with-decryption --query "Parameter.Value" --out text) + - export GPG_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.gpg_passphrase --with-decryption --query "Parameter.Value" --out text) + - export CENTRAL_PUBLISHER_USERNAME=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.publishing.central_username --with-decryption --query "Parameter.Value" --out text) + - export CENTRAL_PUBLISHER_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.publishing.central_password --with-decryption --query "Parameter.Value" --out text) + - export GPG_PUBLIC_FINGERPRINT=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.gpg_public_key --with-decryption --query "Parameter.Value" --out text | gpg --import --import-options show-only | grep -E -o -e "[A-F0-9]{40}") + +# CI IMAGE + +ci-image: + stage: ci-image + when: manual + except: [ tags, schedules ] + tags: [ "arch:amd64" ] + image: 486234852809.dkr.ecr.us-east-1.amazonaws.com/docker:24.0.4-jammy + script: + - docker buildx build --tag $CI_IMAGE_DOCKER -f ./ci/Dockerfile.gitlab --push . + +# SECURITY + +create_key: + stage: security + when: manual + tags: [ "arch:amd64" ] + variables: + PROJECT_NAME: "dd-sdk-android" + EXPORT_TO_KEYSERVER: "true" + image: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/agent-key-management-tools/gpg:1 + script: + - /create.sh + artifacts: + expire_in: 13 mos + paths: + - pubkeys + + +# STATIC ANALYSIS + +static-analysis: + stage: analysis + variables: + DETEKT_PUBLIC_API: "true" + DETEKT_GENERATE_CLASSPATH_BUILD_TASK: "printSdkDebugRuntimeClasspath" + DETEKT_CLASSPATH_FILE_PATH: "sdk_classpath" + FLAVORED_ANDROID_LINT: ":tools:lint:lint" + trigger: + include: "https://gitlab-templates.ddbuild.io/mobile/v34714656-060be019/static-analysis.yml" + strategy: depend + +analysis:detekt-custom: + tags: + - "arch:amd64" + image: $CI_IMAGE_DOCKER + stage: analysis + timeout: 1h + script: + - ./gradlew assembleLibrariesRelease --stacktrace + - ./gradlew unzipAarForDetekt --stacktrace + - ./gradlew :tools:detekt:jar --stacktrace + - ./gradlew printDetektClasspath --stacktrace + - curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.4/detekt-cli-1.23.4-all.jar + - ./gradlew :dd-sdk-android-core:customDetektRules + - ./gradlew :dd-sdk-android-internal:customDetektRules + - ./gradlew :features:dd-sdk-android-logs:customDetektRules + - ./gradlew :features:dd-sdk-android-ndk:customDetektRules + - ./gradlew :features:dd-sdk-android-rum:customDetektRules + - ./gradlew :features:dd-sdk-android-session-replay:customDetektRules + - ./gradlew :features:dd-sdk-android-session-replay-material:customDetektRules + - ./gradlew :features:dd-sdk-android-session-replay-compose:customDetektRules + - ./gradlew :features:dd-sdk-android-trace-internal:customDetektRules + - ./gradlew :features:dd-sdk-android-trace:customDetektRules + - ./gradlew :features:dd-sdk-android-trace-otel:customDetektRules + - ./gradlew :features:dd-sdk-android-webview:customDetektRules + - ./gradlew :integrations:dd-sdk-android-coil:customDetektRules + - ./gradlew :integrations:dd-sdk-android-compose:customDetektRules + - ./gradlew :integrations:dd-sdk-android-fresco:customDetektRules + - ./gradlew :integrations:dd-sdk-android-glide:customDetektRules + - ./gradlew :integrations:dd-sdk-android-okhttp:customDetektRules + - ./gradlew :integrations:dd-sdk-android-okhttp-otel:customDetektRules + - ./gradlew :integrations:dd-sdk-android-rum-coroutines:customDetektRules + - ./gradlew :integrations:dd-sdk-android-rx:customDetektRules + - ./gradlew :integrations:dd-sdk-android-sqldelight:customDetektRules + - ./gradlew :integrations:dd-sdk-android-timber:customDetektRules + - ./gradlew :integrations:dd-sdk-android-trace-coroutines:customDetektRules + - ./gradlew :integrations:dd-sdk-android-tv:customDetektRules + + +# TODO RUM-1622 cleanup this section +# TESTS + +test:debug: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test + timeout: 1h + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - cache/caches/ + - cache/notifications/ + policy: pull + script: + - rm -rf ~/.gradle/daemon/ + - export DD_AGENT_HOST="$BUILDENV_HOST_IP" + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:debug" ./gradlew :dd-sdk-android-core:testDebugUnitTest --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:debug" ./gradlew :dd-sdk-android-internal:testDebugUnitTest --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:debug" ./gradlew :unitTestDebugFeatures --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:debug" ./gradlew :unitTestDebugIntegrations --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:debug" ./gradlew :unitTestDebugSamples --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + artifacts: + when: always + expire_in: 1 week + reports: + junit: "**/build/test-results/testDebugUnitTest/*.xml" + +test:tools: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test + timeout: 1h + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - cache/caches/ + - cache/notifications/ + policy: pull + script: + - rm -rf ~/.gradle/daemon/ + - export DD_AGENT_HOST="$BUILDENV_HOST_IP" + - GRADLE_OPTS="-Xmx3072m" ./gradlew :unitTestTools --stacktrace --no-daemon --build-cache --gradle-user-home cache/ + +test:kover: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test + timeout: 1h + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - cache/caches/ + - cache/notifications/ + script: + - pip3 install datadog + - rm -rf ~/.gradle/daemon/ + - export DD_AGENT_HOST="$BUILDENV_HOST_IP" + - export DD_API_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.api_key --with-decryption --query "Parameter.Value" --out text) + - export DD_APP_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.app_key --with-decryption --query "Parameter.Value" --out text) + - CODECOV_TOKEN=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.codecov-token --with-decryption --query "Parameter.Value" --out text) + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :dd-sdk-android-core:koverXmlReportRelease --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :dd-sdk-android-internal:koverXmlReportRelease --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :koverReportFeatures --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :koverReportIntegrations --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + - bash <(cat ./ci/scripts/codecov.sh) -t $CODECOV_TOKEN + artifacts: + when: always + expire_in: 1 week + reports: + junit: "**/build/test-results/testReleaseUnitTest/*.xml" + +# TEST PYRAMID +# the steps in this section should reflect our test pyramid strategy + +test-pyramid:core-it-min-api: + tags: [ "macos:sonoma", "specific:true" ] + stage: test-pyramid + timeout: 1h + variables: + ANDROID_API: "23" + ANDROID_EMULATOR_IMAGE: "system-images;android-$ANDROID_API;google_apis;${ANDROID_ARCH}" + ANDROID_PLATFORM: "platforms;android-$ANDROID_API" + ANDROID_BUILD_TOOLS: "build-tools;$ANDROID_API.0.0" + script: + - !reference [.snippets, install-android-api-components] + - !reference [.snippets, run-core-it-instrumented] + +test-pyramid:core-it-latest-api: + tags: [ "macos:sonoma", "specific:true" ] + stage: test-pyramid + timeout: 1h + variables: + ANDROID_API: "36" + ANDROID_EMULATOR_IMAGE: "system-images;android-$ANDROID_API;google_apis;${ANDROID_ARCH}" + ANDROID_PLATFORM: "platforms;android-$ANDROID_API" + ANDROID_BUILD_TOOLS: "build-tools;$ANDROID_API.0.0" + script: + - !reference [.snippets, install-android-api-components] + - !reference [.snippets, run-core-it-instrumented] + +test-pyramid:core-it-median-api: + tags: [ "macos:sonoma", "specific:true" ] + stage: test-pyramid + timeout: 1h + variables: + ANDROID_API: "28" + ANDROID_EMULATOR_IMAGE: "system-images;android-$ANDROID_API;google_apis;${ANDROID_ARCH}" + ANDROID_PLATFORM: "platforms;android-$ANDROID_API" + ANDROID_BUILD_TOOLS: "build-tools;$ANDROID_API.0.0" + script: + - !reference [.snippets, install-android-api-components] + - !reference [.snippets, run-core-it-instrumented] + +test-pyramid:single-fit-logs: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - cache/caches/ + - cache/notifications/ + policy: pull + script: + - rm -rf ~/.gradle/daemon/ + - export DD_AGENT_HOST="$BUILDENV_HOST_IP" + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :reliability:single-fit:logs:testReleaseUnitTest --stacktrace --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + artifacts: + when: always + expire_in: 1 week + reports: + junit: "**/build/test-results/testReleaseUnitTest/*.xml" + +test-pyramid:single-fit-rum: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - cache/caches/ + - cache/notifications/ + policy: pull + script: + - rm -rf ~/.gradle/daemon/ + - export DD_AGENT_HOST="$BUILDENV_HOST_IP" + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :reliability:single-fit:rum:testReleaseUnitTest --stacktrace --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + artifacts: + when: always + expire_in: 1 week + reports: + junit: "**/build/test-results/testReleaseUnitTest/*.xml" + +test-pyramid:single-fit-trace: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - cache/caches/ + - cache/notifications/ + policy: pull + script: + - rm -rf ~/.gradle/daemon/ + - export DD_AGENT_HOST="$BUILDENV_HOST_IP" + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :reliability:single-fit:trace:testReleaseUnitTest --stacktrace --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + artifacts: + when: always + expire_in: 1 week + reports: + junit: "**/build/test-results/testReleaseUnitTest/*.xml" + +test-pyramid:single-fit-okhttp: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - cache/caches/ + - cache/notifications/ + policy: pull + script: + - rm -rf ~/.gradle/daemon/ + - export DD_AGENT_HOST="$BUILDENV_HOST_IP" + - GRADLE_OPTS="-Xmx3072m" DD_TAGS="test.configuration.variant:release" ./gradlew :reliability:single-fit:okhttp:testReleaseUnitTest --stacktrace --no-daemon --build-cache --gradle-user-home cache/ -Dorg.gradle.jvmargs=-javaagent:$DD_TRACER_FOLDER/dd-java-agent.jar=$DD_COMMON_AGENT_CONFIG + artifacts: + when: always + expire_in: 1 week + reports: + junit: "**/build/test-results/testReleaseUnitTest/*.xml" + +# RUN INSTRUMENTED TESTS ON MIN API (23), LATEST API (34) and MEDIAN API (28) + +test-pyramid:legacy-integration-instrumented-min-api: + tags: [ "macos:sonoma", "specific:true" ] + stage: test-pyramid + timeout: 1h + variables: + ANDROID_API: "23" + ANDROID_EMULATOR_IMAGE: "system-images;android-$ANDROID_API;google_apis;${ANDROID_ARCH}" + ANDROID_PLATFORM: "platforms;android-$ANDROID_API" + ANDROID_BUILD_TOOLS: "build-tools;$ANDROID_API.0.0" + script: + - !reference [.snippets, install-android-api-components] + - !reference [.snippets, run-legacy-integration-instrumented] + +test-pyramid:legacy-integration-instrumented-latest-api: + tags: [ "macos:sonoma", "specific:true" ] + stage: test-pyramid + timeout: 1h + variables: + ANDROID_API: "36" + ANDROID_EMULATOR_IMAGE: "system-images;android-$ANDROID_API;google_apis;${ANDROID_ARCH}" + ANDROID_PLATFORM: "platforms;android-$ANDROID_API" + ANDROID_BUILD_TOOLS: "build-tools;$ANDROID_API.0.0" + script: + - !reference [.snippets, install-android-api-components] + - !reference [.snippets, run-legacy-integration-instrumented] + +test-pyramid:legacy-integration-instrumented-median-api: + tags: [ "macos:sonoma", "specific:true" ] + stage: test-pyramid + timeout: 1h + variables: + ANDROID_API: "28" + ANDROID_EMULATOR_IMAGE: "system-images;android-$ANDROID_API;google_apis;${ANDROID_ARCH}" + ANDROID_PLATFORM: "platforms;android-$ANDROID_API" + ANDROID_BUILD_TOOLS: "build-tools;$ANDROID_API.0.0" + script: + - !reference [.snippets, install-android-api-components] + - !reference [.snippets, run-legacy-integration-instrumented] + +test-pyramid:detekt-api-coverage: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + script: + - mkdir -p ./config/ + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties + - GRADLE_OPTS="-Xmx4096M" ./gradlew assembleLibrariesDebug --stacktrace --no-daemon + - GRADLE_OPTS="-Xmx4096M" ./gradlew printSdkDebugRuntimeClasspath --stacktrace --no-daemon + - GRADLE_OPTS="-Xmx4096M" ./gradlew :tools:detekt:jar --stacktrace --no-daemon + - curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.4/detekt-cli-1.23.4-all.jar + - java -jar detekt-cli-1.23.4-all.jar --config detekt_test_pyramid.yml --plugins tools/detekt/build/libs/detekt.jar -ex "**/*.kts" --jvm-target 11 -cp $(cat sdk_classpath) + # For now we just print the uncovered apis, eventually we will fail if it's not empty + - grep -v -f apiUsage.log apiSurface.log + +test-pyramid:publish-e2e-synthetics: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + only: + - develop + script: + - mkdir -p ./config/ + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore --with-decryption --query "Parameter.Value" --out text | base64 -d > ./sample-android.keystore + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_config_json --with-decryption --query "Parameter.Value" --out text > ./config/us1.json + - export E2E_STORE_PASSWD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore-password --with-decryption --query "Parameter.Value" --out text) + - export E2E_DD_API_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_api_key --with-decryption --query "Parameter.Value" --out text) + - export E2E_DD_APP_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_app_key --with-decryption --query "Parameter.Value" --out text) + - export E2E_MOBILE_APP_ID=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_mobile_app_id --with-decryption --query "Parameter.Value" --out text) + - GRADLE_OPTS="-Xmx4096M" ./gradlew assembleLibrariesRelease --stacktrace --no-daemon + - GRADLE_OPTS="-Xmx4096M" ./gradlew :sample:kotlin:packageUs1Release --stacktrace --no-daemon + - npm update -g @datadog/datadog-ci + - echo "Using datadog-ci $(npx @datadog/datadog-ci version)" + - npx @datadog/datadog-ci synthetics upload-application --appKey "$E2E_DD_APP_KEY" --apiKey "$E2E_DD_API_KEY" --mobileApp "sample/kotlin/build/outputs/apk/us1/release/kotlin-us1-release.apk" --mobileApplicationId "$E2E_MOBILE_APP_ID" --versionName "$CI_COMMIT_SHORT_SHA" --latest + artifacts: + when: always + expire_in: 1 week + paths: + - sample/kotlin/build/outputs/apk/us1/release/kotlin-us1-release.apk + +test-pyramid:publish-webview-synthetics: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + only: + - develop + script: + - mkdir -p ./config/ + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore --with-decryption --query "Parameter.Value" --out text | base64 -d > ./sample-android.keystore + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.webview_config_json --with-decryption --query "Parameter.Value" --out text > ./config/us1.json + - export E2E_STORE_PASSWD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore-password --with-decryption --query "Parameter.Value" --out text) + - export E2E_DD_API_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.webview_api_key --with-decryption --query "Parameter.Value" --out text) + - export E2E_DD_APP_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.webview_app_key --with-decryption --query "Parameter.Value" --out text) + - export E2E_MOBILE_APP_ID=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.webview_mobile_app_id --with-decryption --query "Parameter.Value" --out text) + - GRADLE_OPTS="-Xmx4096M" ./gradlew assembleLibrariesRelease --stacktrace --no-daemon + - GRADLE_OPTS="-Xmx4096M" ./gradlew :sample:kotlin:packageUs1Release --stacktrace --no-daemon + - npm update -g @datadog/datadog-ci + - echo "Using datadog-ci $(npx @datadog/datadog-ci version)" + - npx @datadog/datadog-ci synthetics upload-application --appKey "$E2E_DD_APP_KEY" --apiKey "$E2E_DD_API_KEY" --mobileApp "sample/kotlin/build/outputs/apk/us1/release/kotlin-us1-release.apk" --mobileApplicationId "$E2E_MOBILE_APP_ID" --versionName "$CI_COMMIT_SHORT_SHA" --latest + artifacts: + when: always + expire_in: 1 week + paths: + - sample/kotlin/build/outputs/apk/us1/release/kotlin-us1-release.apk + +test-pyramid:publish-staging-synthetics: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + only: + - develop + script: + - mkdir -p ./config/ + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore --with-decryption --query "Parameter.Value" --out text | base64 -d > ./sample-android.keystore + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_staging_config_json --with-decryption --query "Parameter.Value" --out text > ./config/staging.json + - export E2E_STORE_PASSWD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore-password --with-decryption --query "Parameter.Value" --out text) + - export E2E_DD_API_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_staging_api_key --with-decryption --query "Parameter.Value" --out text) + - export E2E_DD_APP_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_staging_app_key --with-decryption --query "Parameter.Value" --out text) + - export E2E_MOBILE_APP_ID=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.e2e_staging_mobile_app_id --with-decryption --query "Parameter.Value" --out text) + - GRADLE_OPTS="-Xmx4096M" ./gradlew assembleLibrariesRelease --stacktrace --no-daemon + - GRADLE_OPTS="-Xmx4096M" ./gradlew :sample:kotlin:packageStagingRelease --stacktrace --no-daemon + - npm update -g @datadog/datadog-ci + - echo "Using datadog-ci $(npx @datadog/datadog-ci version)" + - npx @datadog/datadog-ci synthetics upload-application --appKey "$E2E_DD_APP_KEY" --apiKey "$E2E_DD_API_KEY" --mobileApp "sample/kotlin/build/outputs/apk/staging/release/kotlin-staging-release.apk" --mobileApplicationId "$E2E_MOBILE_APP_ID" --versionName "$CI_COMMIT_SHORT_SHA" --latest --datadogSite "datad0g.com" + artifacts: + when: always + expire_in: 1 week + paths: + - sample/kotlin/build/outputs/apk/staging/release/kotlin-staging-release.apk + +test-pyramid:publish-benchmark-synthetics: + tags: [ "arch:amd64" ] + image: $CI_IMAGE_DOCKER + stage: test-pyramid + timeout: 1h + only: + - develop + script: + - mkdir -p ./config/ + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore --with-decryption --query "Parameter.Value" --out text | base64 -d > ./sample-benchmark.keystore + - aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.benchmark_config_json --with-decryption --query "Parameter.Value" --out text > ./config/benchmark.json + - export BM_STORE_PASSWD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.keystore-password --with-decryption --query "Parameter.Value" --out text) + - export BM_DD_API_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.benchmark_api_key --with-decryption --query "Parameter.Value" --out text) + - export BM_DD_APP_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.benchmark_app_key --with-decryption --query "Parameter.Value" --out text) + - export BM_MOBILE_APP_ID=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.benchmark_mobile_app_id --with-decryption --query "Parameter.Value" --out text) + - GRADLE_OPTS="-Xmx4096M" ./gradlew assembleLibrariesRelease --stacktrace --no-daemon + - GRADLE_OPTS="-Xmx4096M" ./gradlew :sample:benchmark:packageRelease --stacktrace --no-daemon + - npm update -g @datadog/datadog-ci + - echo "Using datadog-ci $(npx @datadog/datadog-ci version)" + - npx @datadog/datadog-ci synthetics upload-application --appKey "$BM_DD_APP_KEY" --apiKey "$BM_DD_API_KEY" --mobileApp "sample/benchmark/build/outputs/apk/release/benchmark-release.apk" --mobileApplicationId "$BM_MOBILE_APP_ID" --versionName "$CI_COMMIT_SHORT_SHA" --latest + artifacts: + when: always + expire_in: 1 week + paths: + - sample/benchmark/build/outputs/apk/release/benchmark-release.apk + +# PUBLISH ARTIFACTS ON MAVEN + +publish:release-core: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :dd-sdk-android-core:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - dd-sdk-android-core/verification-metadata.xml + +publish:release-internal: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [ .snippets, set-publishing-credentials ] + - ./gradlew :dd-sdk-android-internal:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - dd-sdk-android-internal/verification-metadata.xml + +# region Publish features/* +publish:release-trace-api: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-trace-api:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-trace-api/verification-metadata.xml + + +publish:release-trace-internal: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [ .snippets, set-publishing-credentials ] + - ./gradlew :features:dd-sdk-android-trace-internal:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-trace-internal/verification-metadata.xml + +publish:release-trace: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-trace:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-trace/verification-metadata.xml + +publish:release-trace-otel: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-trace-otel:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-trace-otel/verification-metadata.xml + +publish:release-logs: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-logs:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-logs/verification-metadata.xml + +publish:release-rum: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-rum:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-rum/verification-metadata.xml + +publish:release-ndk: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-ndk:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-ndk/verification-metadata.xml + +publish:release-session-replay: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-session-replay:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-session-replay/verification-metadata.xml + +publish:release-session-replay-material: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-session-replay-material:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-session-replay-material/verification-metadata.xml + +publish:release-session-replay-compose: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-session-replay-compose:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-session-replay-compose/verification-metadata.xml + +publish:release-webview: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :features:dd-sdk-android-webview:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - features/dd-sdk-android-webview/verification-metadata.xml + +# endregion + +# region Publish integrations/* + +publish:release-apollo: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-apollo:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-apollo/verification-metadata.xml + +publish:release-coil: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-coil:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-coil/verification-metadata.xml + +publish:release-compose: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-compose:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-compose/verification-metadata.xml + +publish:release-fresco: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-fresco:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-fresco/verification-metadata.xml + +publish:release-glide: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-glide:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-glide/verification-metadata.xml + +publish:release-trace-coroutines: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-trace-coroutines:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-trace-coroutines/verification-metadata.xml + +publish:release-rum-coroutines: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-rum-coroutines:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-rum-coroutines/verification-metadata.xml + +publish:release-rx: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-rx:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-rx/verification-metadata.xml + +publish:release-sqldelight: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-sqldelight:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-sqldelight/verification-metadata.xml + +publish:release-timber: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-timber:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-timber/verification-metadata.xml + +publish:release-android-tv: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-tv:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-tv/verification-metadata.xml + +publish:release-okhttp: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-okhttp:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-okhttp/verification-metadata.xml + +publish:release-okhttp-otel: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :integrations:dd-sdk-android-okhttp-otel:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - integrations/dd-sdk-android-okhttp-otel/verification-metadata.xml + +# endregion + +publish:release-benchmark: + tags: [ "arch:amd64" ] + only: + - tags + - develop + image: $CI_IMAGE_DOCKER + stage: publish + timeout: 30m + script: + - !reference [.snippets, set-publishing-credentials] + - ./gradlew :tools:benchmark:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon + artifacts: + when: on_success + expire_in: 7 days + paths: + - tools/benchmark/verification-metadata.xml + + +# SLACK NOTIFICATIONS + +notify:publish-develop-success: + extends: .slack-notifier-base + stage: notify + when: on_success + only: + - develop + script: + - 'MESSAGE_TEXT=":package: $CI_PROJECT_NAME develop $CI_COMMIT_TAG: Snapshot published on :maven:, Sample app published on :synthetics:"' + - postmessage "#mobile-sdk-ops" "$MESSAGE_TEXT" + +notify:publish-develop-failure: + extends: .slack-notifier-base + stage: notify + when: on_failure + only: + - develop + script: + - BUILD_URL="$CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID" + - 'MESSAGE_TEXT=":status_alert: $CI_PROJECT_NAME $CI_COMMIT_TAG develop pipeline <$BUILD_URL|$COMMIT_MESSAGE> failed."' + - postmessage "#mobile-sdk-ops" "$MESSAGE_TEXT" + +notify:publish-release-success: + extends: .slack-notifier-base + stage: notify + when: on_success + only: + - tags + script: + - MAVEN_URL="https://search.maven.org/artifact/com.datadoghq/dd-sdk-android-core/$CI_COMMIT_TAG/aar" + - 'MESSAGE_TEXT=":rocket: $CI_PROJECT_NAME $CI_COMMIT_TAG published on :maven: $MAVEN_URL"' + - postmessage "#mobile-sdk-ops" "$MESSAGE_TEXT" + +notify:publish-release-failure: + extends: .slack-notifier-base + stage: notify + when: on_failure + only: + - tags + script: + - BUILD_URL="$CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID" + - 'MESSAGE_TEXT=":status_alert: $CI_PROJECT_NAME $CI_COMMIT_TAG publish pipeline <$BUILD_URL|$COMMIT_MESSAGE> failed."' + - postmessage "#mobile-sdk-ops" "$MESSAGE_TEXT" + +notify:dogfood-app: + tags: [ "arch:amd64" ] + only: + - tags + image: $CI_IMAGE_DOCKER + id_tokens: + DDOCTOSTS_ID_TOKEN: + aud: dd-octo-sts + stage: notify + when: on_success + script: + - export GITHUB_TOKEN=$(dd-octo-sts token --scope DataDog/datadog-android --policy all.gitlab.pr) + - pip3 install GitPython requests + - python3 ./ci/scripts/dogfood.py -v $CI_COMMIT_TAG -t app + +notify:dogfood-demo: + tags: [ "arch:amd64" ] + only: + - tags + image: $CI_IMAGE_DOCKER + id_tokens: + DDOCTOSTS_ID_TOKEN: + aud: dd-octo-sts + stage: notify + when: on_success + script: + - export GITHUB_TOKEN=$(dd-octo-sts token --scope DataDog/shopist-android --policy all.gitlab.pr) + - pip3 install GitPython requests + - python3 ./ci/scripts/dogfood.py -v $CI_COMMIT_TAG -t demo + +notify:dogfood-gradle-plugin: + tags: [ "arch:amd64" ] + only: + - tags + image: $CI_IMAGE_DOCKER + id_tokens: + DDOCTOSTS_ID_TOKEN: + aud: dd-octo-sts + stage: notify + when: on_success + script: + - export GITHUB_TOKEN=$(dd-octo-sts token --scope DataDog/dd-sdk-android-gradle-plugin --policy all.gitlab.pr) + - pip3 install GitPython requests + - python3 ./ci/scripts/dogfood.py -v $CI_COMMIT_TAG -t gradle-plugin + +notify:merge-verification-metadata: + tags: [ "arch:amd64" ] + only: + - tags + image: $CI_IMAGE_DOCKER + stage: notify + when: on_success + dependencies: + - publish:release-core + - publish:release-internal + - publish:release-trace + - publish:release-trace-otel + - publish:release-logs + - publish:release-rum + - publish:release-ndk + - publish:release-session-replay + - publish:release-session-replay-material + - publish:release-session-replay-compose + - publish:release-webview + - publish:release-coil + - publish:release-compose + - publish:release-fresco + - publish:release-glide + - publish:release-trace-coroutines + - publish:release-rum-coroutines + - publish:release-rx + - publish:release-sqldelight + - publish:release-timber + - publish:release-android-tv + - publish:release-okhttp + - publish:release-okhttp-otel + - publish:release-benchmark + script: + - python3 ./ci/scripts/merge_verification_metadata.py + artifacts: + when: on_success + expire_in: 3 mos + paths: + - verification-metadata.xml + diff --git a/ci/scripts/check_latest_release_is_published.sh b/ci/scripts/check_latest_release_is_published.sh new file mode 100644 index 0000000000..421da1dcec --- /dev/null +++ b/ci/scripts/check_latest_release_is_published.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2016-Present Datadog, Inc. +# + +set -o pipefail + +tag_name=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/DataDog/dd-sdk-android/releases/latest | jq -r .tag_name) + +if [ -z "$tag_name" ] || [ "$tag_name" = "null" ]; then + echo "Error: Failed to retrieve tag_name from GitHub API: tag_name='$tag_name'" + exit 1 +fi + +for artifactId in $(./gradlew -q listAllPublishedArtifactIds); do + artifact_url="https://repo1.maven.org/maven2/com/datadoghq/$artifactId/$tag_name/$artifactId-$tag_name.aar" + status_code=$(curl -s -o /dev/null -w "%{http_code}" "$artifact_url") + + if [ $status_code -eq 200 ]; then + echo "Release $tag_name exists for $artifactId" + exit 0 + elif [ $status_code -eq 404 ]; then + echo "Release $tag_name doesn't exist for $artifactId" + echo "URL: $artifact_url" + exit 1 + else + echo "Error: Unexpected status code $status_code when checking for $artifactId" + echo "URL: $artifact_url" + exit 1 + fi +done diff --git a/ci/scripts/codecov.sh b/ci/scripts/codecov.sh new file mode 100644 index 0000000000..486d40a127 --- /dev/null +++ b/ci/scripts/codecov.sh @@ -0,0 +1,1884 @@ +#!/usr/bin/env bash + +# Apache License Version 2.0, January 2004 +# https://github.com/codecov/codecov-bash/blob/master/LICENSE + +set -e +o pipefail + +VERSION="1.0.1" + +codecov_flags=( ) +url="https://codecov.io/" +env="$CODECOV_ENV" +service="" +token="" +search_in="" +# shellcheck disable=SC2153 +flags="$CODECOV_FLAGS" +exit_with=0 +curlargs="" +curlawsargs="" +dump="0" +clean="0" +curl_s="-s" +name="$CODECOV_NAME" +include_cov="" +exclude_cov="" +ddp="$HOME/Library/Developer/Xcode/DerivedData" +xp="" +files="" +save_to="" +direct_file_upload="" +cacert="$CODECOV_CA_BUNDLE" +gcov_ignore="-not -path './bower_components/**' -not -path './node_modules/**' -not -path './vendor/**'" +gcov_include="" + +ft_gcov="1" +ft_coveragepy="1" +ft_fix="1" +ft_search="1" +ft_s3="1" +ft_network="1" +ft_xcodellvm="1" +ft_xcodeplist="0" +ft_gcovout="1" +ft_html="0" +ft_yaml="0" + +_git_root=$(git rev-parse --show-toplevel 2>/dev/null || hg root 2>/dev/null || echo "$PWD") +git_root="$_git_root" +remote_addr="" +if [ "$git_root" = "$PWD" ]; +then + git_root="." +fi + +branch_o="" +build_o="" +commit_o="" +pr_o="" +prefix_o="" +network_filter_o="" +search_in_o="" +slug_o="" +tag_o="" +url_o="" +git_ls_files_recurse_submodules_o="" +package="bash" + +commit="$VCS_COMMIT_ID" +branch="$VCS_BRANCH_NAME" +pr="$VCS_PULL_REQUEST" +slug="$VCS_SLUG" +tag="$VCS_TAG" +build_url="$CI_BUILD_URL" +build="$CI_JOB_ID" +job="$CI_JOB_ID" + +beta_xcode_partials="" + +proj_root="$git_root" +gcov_exe="gcov" +gcov_arg="" + +b="\033[0;36m" +g="\033[0;32m" +r="\033[0;31m" +e="\033[0;90m" +y="\033[0;33m" +x="\033[0m" + +show_help() { +cat << EOF + + Codecov Bash $VERSION + + Global report uploading tool for Codecov + Documentation at https://docs.codecov.io/docs + Contribute at https://github.com/codecov/codecov-bash + + + -h Display this help and exit + -f FILE Target file(s) to upload + + -f "path/to/file" only upload this file + skips searching unless provided patterns below + + -f '!*.bar' ignore all files at pattern *.bar + -f '*.foo' include all files at pattern *.foo + Must use single quotes. + This is non-exclusive, use -s "*.foo" to match specific paths. + + -s DIR Directory to search for coverage reports. + Already searches project root and artifact folders. + -t TOKEN Set the private repository token + (option) set environment variable CODECOV_TOKEN=:uuid + + -t @/path/to/token_file + -t uuid + + -n NAME Custom defined name of the upload. Visible in Codecov UI + + -e ENV Specify environment variables to be included with this build + Also accepting environment variables: CODECOV_ENV=VAR,VAR2 + + -e VAR,VAR2 + + -k prefix Prefix filepaths to help resolve path fixing + + -i prefix Only include files in the network with a certain prefix. Useful for upload-specific path fixing + + -X feature Toggle functionalities + + -X gcov Disable gcov + -X coveragepy Disable python coverage + -X fix Disable report fixing + -X search Disable searching for reports + -X xcode Disable xcode processing + -X network Disable uploading the file network + -X gcovout Disable gcov output + -X html Enable coverage for HTML files + -X recursesubs Enable recurse submodules in git projects when searching for source files + -X yaml Enable coverage for YAML files + + -N The commit SHA of the parent for which you are uploading coverage. If not present, + the parent will be determined using the API of your repository provider. + When using the repository provider's API, the parent is determined via finding + the closest ancestor to the commit. + + -R root dir Used when not in git/hg project to identify project root directory + -F flag Flag the upload to group coverage metrics + + -F unittests This upload is only unittests + -F integration This upload is only integration tests + -F ui,chrome This upload is Chrome - UI tests + + -c Move discovered coverage reports to the trash + -z FILE Upload specified file directly to Codecov and bypass all report generation. + This is inteded to be used only with a pre-formatted Codecov report and is not + expected to work under any other circumstances. + -Z Exit with 1 if not successful. Default will Exit with 0 + + -- xcode -- + -D Custom Derived Data Path for Coverage.profdata and gcov processing + Default '~/Library/Developer/Xcode/DerivedData' + -J Specify packages to build coverage. Uploader will only build these packages. + This can significantly reduces time to build coverage reports. + + -J 'MyAppName' Will match "MyAppName" and "MyAppNameTests" + -J '^ExampleApp$' Will match only "ExampleApp" not "ExampleAppTests" + + -- gcov -- + -g GLOB Paths to ignore during gcov gathering + -G GLOB Paths to include during gcov gathering + -p dir Project root directory + Also used when preparing gcov + -x gcovexe gcov executable to run. Defaults to 'gcov' + -a gcovargs extra arguments to pass to gcov + + -- Override CI Environment Variables -- + These variables are automatically detected by popular CI providers + + -B branch Specify the branch name + -C sha Specify the commit sha + -P pr Specify the pull request number + -b build Specify the build number + -T tag Specify the git tag + + -- Enterprise -- + -u URL Set the target url for Enterprise customers + Not required when retrieving the bash uploader from your CCE + (option) Set environment variable CODECOV_URL=https://my-hosted-codecov.com + -r SLUG owner/repo slug used instead of the private repo token in Enterprise + (option) set environment variable CODECOV_SLUG=:owner/:repo + (option) set in your codecov.yml "codecov.slug" + -S PATH File path to your cacert.pem file used to verify ssl with Codecov Enterprise (optional) + (option) Set environment variable: CODECOV_CA_BUNDLE="/path/to/ca.pem" + -U curlargs Extra curl arguments to communicate with Codecov. e.g., -U "--proxy http://http-proxy" + -A curlargs Extra curl arguments to communicate with AWS. + + -- Debugging -- + -d Don't upload, but dump upload file to stdout + -q PATH Write upload file to path + -K Remove color from the output + -v Verbose mode + +EOF +} + + +say() { + echo -e "$1" +} + + +urlencode() { + echo "$1" | curl -Gso /dev/null -w "%{url_effective}" --data-urlencode @- "" | cut -c 3- | sed -e 's/%0A//' +} + +swiftcov() { + _dir=$(dirname "$1" | sed 's/\(Build\).*/\1/g') + for _type in app framework xctest + do + find "$_dir" -name "*.$_type" | while read -r f + do + _proj=${f##*/} + _proj=${_proj%."$_type"} + if [ "$2" = "" ] || [ "$(echo "$_proj" | grep -i "$2")" != "" ]; + then + say " $g+$x Building reports for $_proj $_type" + dest=$([ -f "$f/$_proj" ] && echo "$f/$_proj" || echo "$f/Contents/MacOS/$_proj") + # shellcheck disable=SC2001 + _proj_name=$(echo "$_proj" | sed -e 's/[[:space:]]//g') + # shellcheck disable=SC2086 + xcrun llvm-cov show $beta_xcode_partials -instr-profile "$1" "$dest" > "$_proj_name.$_type.coverage.txt" \ + || say " ${r}x>${x} llvm-cov failed to produce results for $dest" + fi + done + done +} + + +# Credits to: https://gist.github.com/pkuczynski/8665367 +parse_yaml() { + local prefix=$2 + local s='[[:space:]]*' w='[a-zA-Z0-9_]*' + local fs + fs=$(echo @|tr @ '\034') + sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ + -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" | + awk -F"$fs" '{ + indent = length($1)/2; + vname[indent] = $2; + for (i in vname) {if (i > indent) {delete vname[i]}} + if (length($3) > 0) { + vn=""; if (indent > 0) {vn=(vn)(vname[0])("_")} + printf("%s%s%s=\"%s\"\n", "'"$prefix"'",vn, $2, $3); + } + }' +} + +if [ $# != 0 ]; +then + while getopts "a:A:b:B:cC:dD:e:f:F:g:G:hi:J:k:Kn:p:P:Q:q:r:R:s:S:t:T:u:U:vx:X:Zz:N:-" o + do + codecov_flags+=( "$o" ) + case "$o" in + "-") + echo -e "${r}Long options are not supported${x}" + exit 2 + ;; + "?") + ;; + "N") + parent=$OPTARG + ;; + "a") + gcov_arg=$OPTARG + ;; + "A") + curlawsargs="$OPTARG" + ;; + "b") + build_o="$OPTARG" + ;; + "B") + branch_o="$OPTARG" + ;; + "c") + clean="1" + ;; + "C") + commit_o="$OPTARG" + ;; + "d") + dump="1" + ;; + "D") + ddp="$OPTARG" + ;; + "e") + env="$env,$OPTARG" + ;; + "f") + if [ "${OPTARG::1}" = "!" ]; + then + exclude_cov="$exclude_cov -not -path '${OPTARG:1}'" + + elif [[ "$OPTARG" = *"*"* ]]; + then + include_cov="$include_cov -or -path '$OPTARG'" + + else + ft_search=0 + if [ "$files" = "" ]; + then + files="$OPTARG" + else + files="$files +$OPTARG" + fi + fi + ;; + "F") + if [ "$flags" = "" ]; + then + flags="$OPTARG" + else + flags="$flags,$OPTARG" + fi + ;; + "g") + gcov_ignore="$gcov_ignore -not -path '$OPTARG'" + ;; + "G") + gcov_include="$gcov_include -path '$OPTARG'" + ;; + "h") + show_help + exit 0; + ;; + "i") + network_filter_o="$OPTARG" + ;; + "J") + ft_xcodellvm="1" + ft_xcodeplist="0" + if [ "$xp" = "" ]; + then + xp="$OPTARG" + else + xp="$xp\|$OPTARG" + fi + ;; + "k") + prefix_o=$(echo "$OPTARG" | sed -e 's:^/*::' -e 's:/*$::') + ;; + "K") + b="" + g="" + r="" + e="" + x="" + ;; + "n") + name="$OPTARG" + ;; + "p") + proj_root="$OPTARG" + ;; + "P") + pr_o="$OPTARG" + ;; + "Q") + # this is only meant for Codecov packages to overwrite + package="$OPTARG" + ;; + "q") + save_to="$OPTARG" + ;; + "r") + slug_o="$OPTARG" + ;; + "R") + git_root="$OPTARG" + ;; + "s") + if [ "$search_in_o" = "" ]; + then + search_in_o="$OPTARG" + else + search_in_o="$search_in_o $OPTARG" + fi + ;; + "S") + # shellcheck disable=SC2089 + cacert="--cacert \"$OPTARG\"" + ;; + "t") + if [ "${OPTARG::1}" = "@" ]; + then + token=$(< "${OPTARG:1}" tr -d ' \n') + else + token="$OPTARG" + fi + ;; + "T") + tag_o="$OPTARG" + ;; + "u") + url_o=$(echo "$OPTARG" | sed -e 's/\/$//') + ;; + "U") + curlargs="$OPTARG" + ;; + "v") + set -x + curl_s="" + ;; + "x") + gcov_exe=$OPTARG + ;; + "X") + if [ "$OPTARG" = "gcov" ]; + then + ft_gcov="0" + elif [ "$OPTARG" = "coveragepy" ] || [ "$OPTARG" = "py" ]; + then + ft_coveragepy="0" + elif [ "$OPTARG" = "gcovout" ]; + then + ft_gcovout="0" + elif [ "$OPTARG" = "xcodellvm" ]; + then + ft_xcodellvm="1" + ft_xcodeplist="0" + elif [ "$OPTARG" = "fix" ] || [ "$OPTARG" = "fixes" ]; + then + ft_fix="0" + elif [ "$OPTARG" = "xcode" ]; + then + ft_xcodellvm="0" + ft_xcodeplist="0" + elif [ "$OPTARG" = "search" ]; + then + ft_search="0" + elif [ "$OPTARG" = "xcodepartials" ]; + then + beta_xcode_partials="-use-color" + elif [ "$OPTARG" = "network" ]; + then + ft_network="0" + elif [ "$OPTARG" = "s3" ]; + then + ft_s3="0" + elif [ "$OPTARG" = "html" ]; + then + ft_html="1" + elif [ "$OPTARG" = "recursesubs" ]; + then + git_ls_files_recurse_submodules_o="--recurse-submodules" + elif [ "$OPTARG" = "yaml" ]; + then + ft_yaml="1" + fi + ;; + "Z") + exit_with=1 + ;; + "z") + direct_file_upload="$OPTARG" + ft_gcov="0" + ft_coveragepy="0" + ft_fix="0" + ft_search="0" + ft_network="0" + ft_xcodellvm="0" + ft_gcovout="0" + include_cov="" + ;; + *) + echo -e "${r}Unexpected flag not supported${x}" + ;; + esac + done +fi + +say " + _____ _ + / ____| | | +| | ___ __| | ___ ___ _____ __ +| | / _ \\ / _\` |/ _ \\/ __/ _ \\ \\ / / +| |___| (_) | (_| | __/ (_| (_) \\ V / + \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/ + Bash-$VERSION + +" + +# check for installed tools +# git/hg +if [ "$direct_file_upload" = "" ]; +then + if [ -x "$(command -v git)" ]; + then + say "$b==>$x $(git --version) found" + else + say "$y==>$x git not installed, testing for mercurial" + if [ -x "$(command -v hg)" ]; + then + say "$b==>$x $(hg --version) found" + else + say "$r==>$x git nor mercurial are installed. Uploader may fail or have unintended consequences" + fi + fi +fi +# curl +if [ -x "$(command -v curl)" ]; +then + say "$b==>$x $(curl --version)" +else + say "$r==>$x curl not installed. Exiting." + exit ${exit_with}; +fi + +search_in="$proj_root" + +#shellcheck disable=SC2154 +if [ "$JENKINS_URL" != "" ]; +then + say "$e==>$x Jenkins CI detected." + # https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project + # https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables + service="jenkins" + + # shellcheck disable=SC2154 + if [ "$ghprbSourceBranch" != "" ]; + then + branch="$ghprbSourceBranch" + elif [ "$GIT_BRANCH" != "" ]; + then + branch="$GIT_BRANCH" + elif [ "$BRANCH_NAME" != "" ]; + then + branch="$BRANCH_NAME" + fi + + # shellcheck disable=SC2154 + if [ "$ghprbActualCommit" != "" ]; + then + commit="$ghprbActualCommit" + elif [ "$GIT_COMMIT" != "" ]; + then + commit="$GIT_COMMIT" + fi + + # shellcheck disable=SC2154 + if [ "$ghprbPullId" != "" ]; + then + pr="$ghprbPullId" + elif [ "$CHANGE_ID" != "" ]; + then + pr="$CHANGE_ID" + fi + + build="$BUILD_NUMBER" + # shellcheck disable=SC2153 + build_url=$(urlencode "$BUILD_URL") + +elif [ "$CI" = "true" ] && [ "$TRAVIS" = "true" ] && [ "$SHIPPABLE" != "true" ]; +then + say "$e==>$x Travis CI detected." + # https://docs.travis-ci.com/user/environment-variables/ + service="travis" + commit="${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT}" + build="$TRAVIS_JOB_NUMBER" + pr="$TRAVIS_PULL_REQUEST" + job="$TRAVIS_JOB_ID" + slug="$TRAVIS_REPO_SLUG" + env="$env,TRAVIS_OS_NAME" + tag="$TRAVIS_TAG" + if [ "$TRAVIS_BRANCH" != "$TRAVIS_TAG" ]; + then + branch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}" + fi + + language=$(compgen -A variable | grep "^TRAVIS_.*_VERSION$" | head -1) + if [ "$language" != "" ]; + then + env="$env,${!language}" + fi + +elif [ "$CODEBUILD_CI" = "true" ]; +then + say "$e==>$x AWS Codebuild detected." + # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html + service="codebuild" + commit="$CODEBUILD_RESOLVED_SOURCE_VERSION" + build="$CODEBUILD_BUILD_ID" + branch="$(echo "$CODEBUILD_WEBHOOK_HEAD_REF" | sed 's/^refs\/heads\///')" + if [ "${CODEBUILD_SOURCE_VERSION/pr}" = "$CODEBUILD_SOURCE_VERSION" ] ; then + pr="false" + else + pr="$(echo "$CODEBUILD_SOURCE_VERSION" | sed 's/^pr\///')" + fi + job="$CODEBUILD_BUILD_ID" + slug="$(echo "$CODEBUILD_SOURCE_REPO_URL" | sed 's/^.*:\/\/[^\/]*\///' | sed 's/\.git$//')" + +elif [ "$CI" = "true" ] && [ "$CI_NAME" = "codeship" ]; +then + say "$e==>$x Codeship CI detected." + # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/ + service="codeship" + branch="$CI_BRANCH" + build="$CI_BUILD_NUMBER" + build_url=$(urlencode "$CI_BUILD_URL") + commit="$CI_COMMIT_ID" + +elif [ -n "$CF_BUILD_URL" ] && [ -n "$CF_BUILD_ID" ]; +then + say "$e==>$x Codefresh CI detected." + # https://docs.codefresh.io/v1.0/docs/variables + service="codefresh" + branch="$CF_BRANCH" + build="$CF_BUILD_ID" + build_url=$(urlencode "$CF_BUILD_URL") + commit="$CF_REVISION" + +elif [ "$TEAMCITY_VERSION" != "" ]; +then + say "$e==>$x TeamCity CI detected." + # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters + # https://confluence.jetbrains.com/plugins/servlet/mobile#content/view/74847298 + if [ "$TEAMCITY_BUILD_BRANCH" = '' ]; + then + echo " Teamcity does not automatically make build parameters available as environment variables." + echo " Add the following environment parameters to the build configuration" + echo " env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%" + echo " env.TEAMCITY_BUILD_ID = %teamcity.build.id%" + echo " env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%" + echo " env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%" + echo " env.TEAMCITY_BUILD_REPOSITORY = %vcsroot..url%" + fi + service="teamcity" + branch="$TEAMCITY_BUILD_BRANCH" + build="$TEAMCITY_BUILD_ID" + build_url=$(urlencode "$TEAMCITY_BUILD_URL") + if [ "$TEAMCITY_BUILD_COMMIT" != "" ]; + then + commit="$TEAMCITY_BUILD_COMMIT" + else + commit="$BUILD_VCS_NUMBER" + fi + remote_addr="$TEAMCITY_BUILD_REPOSITORY" + +elif [ "$CI" = "true" ] && [ "$CIRCLECI" = "true" ]; +then + say "$e==>$x Circle CI detected." + # https://circleci.com/docs/environment-variables + service="circleci" + branch="$CIRCLE_BRANCH" + build="$CIRCLE_BUILD_NUM" + job="$CIRCLE_NODE_INDEX" + if [ "$CIRCLE_PROJECT_REPONAME" != "" ]; + then + slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" + else + # git@github.com:owner/repo.git + slug="${CIRCLE_REPOSITORY_URL##*:}" + # owner/repo.git + slug="${slug%%.git}" + fi + pr="${CIRCLE_PULL_REQUEST##*/}" + commit="$CIRCLE_SHA1" + search_in="$search_in $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS" + +elif [ "$BUDDYBUILD_BRANCH" != "" ]; +then + say "$e==>$x buddybuild detected" + # http://docs.buddybuild.com/v6/docs/custom-prebuild-and-postbuild-steps + service="buddybuild" + branch="$BUDDYBUILD_BRANCH" + build="$BUDDYBUILD_BUILD_NUMBER" + build_url="https://dashboard.buddybuild.com/public/apps/$BUDDYBUILD_APP_ID/build/$BUDDYBUILD_BUILD_ID" + # BUDDYBUILD_TRIGGERED_BY + if [ "$ddp" = "$HOME/Library/Developer/Xcode/DerivedData" ]; + then + ddp="/private/tmp/sandbox/${BUDDYBUILD_APP_ID}/bbtest" + fi + +elif [ "${bamboo_planRepository_revision}" != "" ]; +then + say "$e==>$x Bamboo detected" + # https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables + service="bamboo" + commit="${bamboo_planRepository_revision}" + # shellcheck disable=SC2154 + branch="${bamboo_planRepository_branch}" + # shellcheck disable=SC2154 + build="${bamboo_buildNumber}" + # shellcheck disable=SC2154 + build_url="${bamboo_buildResultsUrl}" + # shellcheck disable=SC2154 + remote_addr="${bamboo_planRepository_repositoryUrl}" + +elif [ "$CI" = "true" ] && [ "$BITRISE_IO" = "true" ]; +then + # http://devcenter.bitrise.io/faq/available-environment-variables/ + say "$e==>$x Bitrise CI detected." + service="bitrise" + branch="$BITRISE_GIT_BRANCH" + build="$BITRISE_BUILD_NUMBER" + build_url=$(urlencode "$BITRISE_BUILD_URL") + pr="$BITRISE_PULL_REQUEST" + if [ "$GIT_CLONE_COMMIT_HASH" != "" ]; + then + commit="$GIT_CLONE_COMMIT_HASH" + fi + +elif [ "$CI" = "true" ] && [ "$SEMAPHORE" = "true" ]; +then + say "$e==>$x Semaphore CI detected." +# https://docs.semaphoreci.com/ci-cd-environment/environment-variables/#semaphore-related + service="semaphore" + branch="$SEMAPHORE_GIT_BRANCH" + build="$SEMAPHORE_WORKFLOW_NUMBER" + job="$SEMAPHORE_JOB_ID" + pr="$PULL_REQUEST_NUMBER" + slug="$SEMAPHORE_REPO_SLUG" + commit="$REVISION" + env="$env,SEMAPHORE_TRIGGER_SOURCE" + +elif [ "$CI" = "true" ] && [ "$BUILDKITE" = "true" ]; +then + say "$e==>$x Buildkite CI detected." + # https://buildkite.com/docs/guides/environment-variables + service="buildkite" + branch="$BUILDKITE_BRANCH" + build="$BUILDKITE_BUILD_NUMBER" + job="$BUILDKITE_JOB_ID" + build_url=$(urlencode "$BUILDKITE_BUILD_URL") + slug="$BUILDKITE_PROJECT_SLUG" + commit="$BUILDKITE_COMMIT" + if [[ "$BUILDKITE_PULL_REQUEST" != "false" ]]; then + pr="$BUILDKITE_PULL_REQUEST" + fi + tag="$BUILDKITE_TAG" + +elif [ "$CI" = "drone" ] || [ "$DRONE" = "true" ]; +then + say "$e==>$x Drone CI detected." + # http://docs.drone.io/env.html + # drone commits are not full shas + service="drone.io" + branch="$DRONE_BRANCH" + build="$DRONE_BUILD_NUMBER" + build_url=$(urlencode "${DRONE_BUILD_LINK}") + pr="$DRONE_PULL_REQUEST" + job="$DRONE_JOB_NUMBER" + tag="$DRONE_TAG" + +elif [ "$CI" = "true" ] && [ "$HEROKU_TEST_RUN_BRANCH" != "" ]; +then + say "$e==>$x Heroku CI detected." + # https://devcenter.heroku.com/articles/heroku-ci#environment-variables + service="heroku" + branch="$HEROKU_TEST_RUN_BRANCH" + build="$HEROKU_TEST_RUN_ID" + commit="$HEROKU_TEST_RUN_COMMIT_VERSION" + +elif [[ "$CI" = "true" || "$CI" = "True" ]] && [[ "$APPVEYOR" = "true" || "$APPVEYOR" = "True" ]]; +then + say "$e==>$x Appveyor CI detected." + # http://www.appveyor.com/docs/environment-variables + service="appveyor" + branch="$APPVEYOR_REPO_BRANCH" + build=$(urlencode "$APPVEYOR_JOB_ID") + pr="$APPVEYOR_PULL_REQUEST_NUMBER" + job="$APPVEYOR_ACCOUNT_NAME%2F$APPVEYOR_PROJECT_SLUG%2F$APPVEYOR_BUILD_VERSION" + slug="$APPVEYOR_REPO_NAME" + commit="$APPVEYOR_REPO_COMMIT" + build_url=$(urlencode "${APPVEYOR_URL}/project/${APPVEYOR_REPO_NAME}/builds/$APPVEYOR_BUILD_ID/job/${APPVEYOR_JOB_ID}") + +elif [ "$CI" = "true" ] && [ "$WERCKER_GIT_BRANCH" != "" ]; +then + say "$e==>$x Wercker CI detected." + # http://devcenter.wercker.com/articles/steps/variables.html + service="wercker" + branch="$WERCKER_GIT_BRANCH" + build="$WERCKER_MAIN_PIPELINE_STARTED" + slug="$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY" + commit="$WERCKER_GIT_COMMIT" + +elif [ "$CI" = "true" ] && [ "$MAGNUM" = "true" ]; +then + say "$e==>$x Magnum CI detected." + # https://magnum-ci.com/docs/environment + service="magnum" + branch="$CI_BRANCH" + build="$CI_BUILD_NUMBER" + commit="$CI_COMMIT" + +elif [ "$SHIPPABLE" = "true" ]; +then + say "$e==>$x Shippable CI detected." + # http://docs.shippable.com/ci_configure/ + service="shippable" + # shellcheck disable=SC2153 + branch=$([ "$HEAD_BRANCH" != "" ] && echo "$HEAD_BRANCH" || echo "$BRANCH") + build="$BUILD_NUMBER" + build_url=$(urlencode "$BUILD_URL") + pr="$PULL_REQUEST" + slug="$REPO_FULL_NAME" + # shellcheck disable=SC2153 + commit="$COMMIT" + +elif [ "$TDDIUM" = "true" ]; +then + say "Solano CI detected." + # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/ + service="solano" + commit="$TDDIUM_CURRENT_COMMIT" + branch="$TDDIUM_CURRENT_BRANCH" + build="$TDDIUM_TID" + pr="$TDDIUM_PR_ID" + +elif [ "$GREENHOUSE" = "true" ]; +then + say "$e==>$x Greenhouse CI detected." + # http://docs.greenhouseci.com/docs/environment-variables-files + service="greenhouse" + branch="$GREENHOUSE_BRANCH" + build="$GREENHOUSE_BUILD_NUMBER" + build_url=$(urlencode "$GREENHOUSE_BUILD_URL") + pr="$GREENHOUSE_PULL_REQUEST" + commit="$GREENHOUSE_COMMIT" + search_in="$search_in $GREENHOUSE_EXPORT_DIR" + +elif [ "$GITLAB_CI" != "" ]; +then + say "$e==>$x GitLab CI detected." + # http://doc.gitlab.com/ce/ci/variables/README.html + service="gitlab" + branch="${CI_BUILD_REF_NAME:-$CI_COMMIT_REF_NAME}" + build="${CI_BUILD_ID:-$CI_JOB_ID}" + remote_addr="${CI_BUILD_REPO:-$CI_REPOSITORY_URL}" + commit="${CI_BUILD_REF:-$CI_COMMIT_SHA}" + slug="${CI_PROJECT_PATH}" + +elif [ "$GITHUB_ACTIONS" != "" ]; +then + say "$e==>$x GitHub Actions detected." + say " Env vars used:" + say " -> GITHUB_ACTIONS: ${GITHUB_ACTIONS}" + say " -> GITHUB_HEAD_REF: ${GITHUB_HEAD_REF}" + say " -> GITHUB_REF: ${GITHUB_REF}" + say " -> GITHUB_REPOSITORY: ${GITHUB_REPOSITORY}" + say " -> GITHUB_RUN_ID: ${GITHUB_RUN_ID}" + say " -> GITHUB_SHA: ${GITHUB_SHA}" + say " -> GITHUB_WORKFLOW: ${GITHUB_WORKFLOW}" + + # https://github.com/features/actions + service="github-actions" + + # https://help.github.com/en/articles/virtual-environments-for-github-actions#environment-variables + branch="${GITHUB_REF#refs/heads/}" + if [ "$GITHUB_HEAD_REF" != "" ]; + then + # PR refs are in the format: refs/pull/7/merge + pr="${GITHUB_REF#refs/pull/}" + pr="${pr%/merge}" + branch="${GITHUB_HEAD_REF}" + fi + commit="${GITHUB_SHA}" + slug="${GITHUB_REPOSITORY}" + build="${GITHUB_RUN_ID}" + build_url=$(urlencode "http://github.com/$%7BGITHUB_REPOSITORY%7D/actions/runs/$%7BGITHUB_RUN_ID%7D") + job="$(urlencode "${GITHUB_WORKFLOW}")" + + # actions/checkout runs in detached HEAD + mc= + if [ -n "$pr" ] && [ "$pr" != false ] && [ "$commit_o" == "" ]; + then + mc=$(git show --no-patch --format="%P" 2>/dev/null || echo "") + + if [[ "$mc" =~ ^[a-z0-9]{40}[[:space:]][a-z0-9]{40}$ ]]; + then + mc=$(echo "$mc" | cut -d' ' -f2) + say " Fixing merge commit SHA $commit -> $mc" + commit=$mc + elif [[ "$mc" = "" ]]; + then + say "$r-> Issue detecting commit SHA. Please run actions/checkout with fetch-depth > 1 or set to 0$x" + fi + fi + +elif [ "$SYSTEM_TEAMFOUNDATIONSERVERURI" != "" ]; +then + say "$e==>$x Azure Pipelines detected." + # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=vsts + # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&viewFallbackFrom=vsts&tabs=yaml + service="azure_pipelines" + commit="$BUILD_SOURCEVERSION" + build="$BUILD_BUILDNUMBER" + if [ -z "$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" ]; + then + pr="$SYSTEM_PULLREQUEST_PULLREQUESTID" + else + pr="$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" + fi + project="${SYSTEM_TEAMPROJECT}" + server_uri="${SYSTEM_TEAMFOUNDATIONSERVERURI}" + job="${BUILD_BUILDID}" + branch="${BUILD_SOURCEBRANCH#"refs/heads/"}" + build_url=$(urlencode "${SYSTEM_TEAMFOUNDATIONSERVERURI}${SYSTEM_TEAMPROJECT}/_build/results?buildId=${BUILD_BUILDID}") + + # azure/pipelines runs in detached HEAD + mc= + if [ -n "$pr" ] && [ "$pr" != false ]; + then + mc=$(git show --no-patch --format="%P" 2>/dev/null || echo "") + + if [[ "$mc" =~ ^[a-z0-9]{40}[[:space:]][a-z0-9]{40}$ ]]; + then + mc=$(echo "$mc" | cut -d' ' -f2) + say " Fixing merge commit SHA $commit -> $mc" + commit=$mc + fi + fi + +elif [ "$CI" = "true" ] && [ "$BITBUCKET_BUILD_NUMBER" != "" ]; +then + say "$e==>$x Bitbucket detected." + # https://confluence.atlassian.com/bitbucket/variables-in-pipelines-794502608.html + service="bitbucket" + branch="$BITBUCKET_BRANCH" + build="$BITBUCKET_BUILD_NUMBER" + slug="$BITBUCKET_REPO_OWNER/$BITBUCKET_REPO_SLUG" + job="$BITBUCKET_BUILD_NUMBER" + pr="$BITBUCKET_PR_ID" + commit="$BITBUCKET_COMMIT" + # See https://jira.atlassian.com/browse/BCLOUD-19393 + if [ "${#commit}" = 12 ]; + then + commit=$(git rev-parse "$BITBUCKET_COMMIT") + fi + +elif [ "$CI" = "true" ] && [ "$BUDDY" = "true" ]; +then + say "$e==>$x Buddy CI detected." + # https://buddy.works/docs/pipelines/environment-variables + service="buddy" + branch="$BUDDY_EXECUTION_BRANCH" + build="$BUDDY_EXECUTION_ID" + build_url=$(urlencode "$BUDDY_EXECUTION_URL") + commit="$BUDDY_EXECUTION_REVISION" + pr="$BUDDY_EXECUTION_PULL_REQUEST_NO" + tag="$BUDDY_EXECUTION_TAG" + slug="$BUDDY_REPO_SLUG" + +elif [ "$CIRRUS_CI" != "" ]; +then + say "$e==>$x Cirrus CI detected." + # https://cirrus-ci.org/guide/writing-tasks/#environment-variables + service="cirrus-ci" + slug="$CIRRUS_REPO_FULL_NAME" + branch="$CIRRUS_BRANCH" + pr="$CIRRUS_PR" + commit="$CIRRUS_CHANGE_IN_REPO" + build="$CIRRUS_BUILD_ID" + build_url=$(urlencode "https://cirrus-ci.com/task/$CIRRUS_TASK_ID") + job="$CIRRUS_TASK_NAME" + +elif [ "$DOCKER_REPO" != "" ]; +then + say "$e==>$x Docker detected." + # https://docs.docker.com/docker-cloud/builds/advanced/ + service="docker" + branch="$SOURCE_BRANCH" + commit="$SOURCE_COMMIT" + slug="$DOCKER_REPO" + tag="$CACHE_TAG" + env="$env,IMAGE_NAME" + +else + say "${r}x>${x} No CI provider detected." + say " Testing inside Docker? ${b}http://docs.codecov.io/docs/testing-with-docker${x}" + say " Testing with Tox? ${b}https://docs.codecov.io/docs/python#section-testing-with-tox${x}" + +fi + +say " ${e}project root:${x} $git_root" + +# find branch, commit, repo from git command +if [ "$GIT_BRANCH" != "" ]; +then + branch="$GIT_BRANCH" + +elif [ "$branch" = "" ]; +then + branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || hg branch 2>/dev/null || echo "") + if [ "$branch" = "HEAD" ]; + then + branch="" + fi +fi + +if [ "$commit_o" = "" ]; +then + if [ "$GIT_COMMIT" != "" ]; + then + commit="$GIT_COMMIT" + elif [ "$commit" = "" ]; + then + commit=$(git log -1 --format="%H" 2>/dev/null || hg id -i --debug 2>/dev/null | tr -d '+' || echo "") + fi +else + commit="$commit_o" +fi + +if [ "$CODECOV_TOKEN" != "" ] && [ "$token" = "" ]; +then + say "${e}-->${x} token set from env" + token="$CODECOV_TOKEN" +fi + +if [ "$CODECOV_URL" != "" ] && [ "$url_o" = "" ]; +then + say "${e}-->${x} url set from env" + url_o=$(echo "$CODECOV_URL" | sed -e 's/\/$//') +fi + +if [ "$CODECOV_SLUG" != "" ]; +then + say "${e}-->${x} slug set from env" + slug_o="$CODECOV_SLUG" + +elif [ "$slug" = "" ]; +then + if [ "$remote_addr" = "" ]; + then + remote_addr=$(git config --get remote.origin.url || hg paths default || echo '') + fi + if [ "$remote_addr" != "" ]; + then + if echo "$remote_addr" | grep -q "//"; then + # https + slug=$(echo "$remote_addr" | cut -d / -f 4,5 | sed -e 's/\.git$//') + else + # ssh + slug=$(echo "$remote_addr" | cut -d : -f 2 | sed -e 's/\.git$//') + fi + fi + if [ "$slug" = "/" ]; + then + slug="" + fi +fi + +yaml=$(cd "$git_root" && \ + git ls-files "*codecov.yml" "*codecov.yaml" 2>/dev/null \ + || hg locate "*codecov.yml" "*codecov.yaml" 2>/dev/null \ + || cd "$proj_root" && find . -maxdepth 1 -type f -name '*codecov.y*ml' 2>/dev/null \ + || echo '') +yaml=$(echo "$yaml" | head -1) + +if [ "$yaml" != "" ]; +then + say " ${e}Yaml found at:${x} $yaml" + if [[ "$yaml" != /* ]]; then + # relative path for yaml file given, assume relative to the repo root + yaml="$git_root/$yaml" + fi + config=$(parse_yaml "$yaml" || echo '') + + # TODO validate the yaml here + + if [ "$(echo "$config" | grep 'codecov_token="')" != "" ] && [ "$token" = "" ]; + then + say "${e}-->${x} token set from yaml" + token="$(echo "$config" | grep 'codecov_token="' | sed -e 's/codecov_token="//' | sed -e 's/"\.*//')" + fi + + if [ "$(echo "$config" | grep 'codecov_url="')" != "" ] && [ "$url_o" = "" ]; + then + say "${e}-->${x} url set from yaml" + url_o="$(echo "$config" | grep 'codecov_url="' | sed -e 's/codecov_url="//' | sed -e 's/"\.*//')" + fi + + if [ "$(echo "$config" | grep 'codecov_slug="')" != "" ] && [ "$slug_o" = "" ]; + then + say "${e}-->${x} slug set from yaml" + slug_o="$(echo "$config" | grep 'codecov_slug="' | sed -e 's/codecov_slug="//' | sed -e 's/"\.*//')" + fi +else + say " ${g}Yaml not found, that's ok! Learn more at${x} ${b}http://docs.codecov.io/docs/codecov-yaml${x}" +fi + +if [ "$branch_o" != "" ]; +then + branch=$(urlencode "$branch_o") +else + branch=$(urlencode "$branch") +fi + +if [ "$slug_o" = "" ]; +then + urlencoded_slug=$(urlencode "$slug") +else + urlencoded_slug=$(urlencode "$slug_o") +fi + +query="branch=$branch\ + &commit=$commit\ + &build=$([ "$build_o" = "" ] && echo "$build" || echo "$build_o")\ + &build_url=$build_url\ + &name=$(urlencode "$name")\ + &tag=$([ "$tag_o" = "" ] && echo "$tag" || echo "$tag_o")\ + &slug=$urlencoded_slug\ + &service=$service\ + &flags=$flags\ + &pr=$([ "$pr_o" = "" ] && echo "${pr##\#}" || echo "${pr_o##\#}")\ + &job=$job\ + &cmd_args=$(IFS=,; echo "${codecov_flags[*]}")" + +if [ -n "$project" ] && [ -n "$server_uri" ]; +then + query=$(echo "$query&project=$project&server_uri=$server_uri" | tr -d ' ') +fi + +if [ "$parent" != "" ]; +then + query=$(echo "parent=$parent&$query" | tr -d ' ') +fi + +if [ "$ft_search" = "1" ]; +then + # detect bower comoponents location + bower_components="bower_components" + bower_rc=$(cd "$git_root" && cat .bowerrc 2>/dev/null || echo "") + if [ "$bower_rc" != "" ]; + then + bower_components=$(echo "$bower_rc" | tr -d '\n' | grep '"directory"' | cut -d'"' -f4 | sed -e 's/\/$//') + if [ "$bower_components" = "" ]; + then + bower_components="bower_components" + fi + fi + + # Swift Coverage + if [ "$ft_xcodellvm" = "1" ] && [ -d "$ddp" ]; + then + say "${e}==>${x} Processing Xcode reports via llvm-cov" + say " DerivedData folder: $ddp" + profdata_files=$(find "$ddp" -name '*.profdata' 2>/dev/null || echo '') + if [ "$profdata_files" != "" ]; + then + # xcode via profdata + if [ "$xp" = "" ]; + then + # xp=$(xcodebuild -showBuildSettings 2>/dev/null | grep -i "^\s*PRODUCT_NAME" | sed -e 's/.*= \(.*\)/\1/') + # say " ${e}->${x} Speed up Xcode processing by adding ${e}-J '$xp'${x}" + say " ${g}hint${x} Speed up Swift processing by using use ${g}-J 'AppName'${x} (regexp accepted)" + say " ${g}hint${x} This will remove Pods/ from your report. Also ${b}https://docs.codecov.io/docs/ignoring-paths${x}" + fi + while read -r profdata; + do + if [ "$profdata" != "" ]; + then + swiftcov "$profdata" "$xp" + fi + done <<< "$profdata_files" + else + say " ${e}->${x} No Swift coverage found" + fi + + # Obj-C Gcov Coverage + if [ "$ft_gcov" = "1" ]; + then + say " ${e}->${x} Running $gcov_exe for Obj-C" + if [ "$ft_gcovout" = "0" ]; + then + # suppress gcov output + bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +" >/dev/null 2>&1 || true + else + bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +" || true + fi + fi + fi + + if [ "$ft_xcodeplist" = "1" ] && [ -d "$ddp" ]; + then + say "${e}==>${x} Processing Xcode plists" + plists_files=$(find "$ddp" -name '*.xccoverage' 2>/dev/null || echo '') + if [ "$plists_files" != "" ]; + then + while read -r plist; + do + if [ "$plist" != "" ]; + then + say " ${g}Found${x} plist file at $plist" + plutil -convert xml1 -o "$(basename "$plist").plist" -- "$plist" + fi + done <<< "$plists_files" + fi + fi + + # Gcov Coverage + if [ "$ft_gcov" = "1" ]; + then + say "${e}==>${x} Running $gcov_exe in $proj_root ${e}(disable via -X gcov)${x}" + if [ "$ft_gcovout" = "0" ]; + then + # suppress gcov output + bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" >/dev/null 2>&1 || true + else + bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" || true + fi + else + say "${e}==>${x} gcov disabled" + fi + + # Python Coverage + if [ "$ft_coveragepy" = "1" ]; + then + if [ ! -f coverage.xml ]; + then + if command -v coverage >/dev/null 2>&1; + then + say "${e}==>${x} Python coveragepy exists ${e}disable via -X coveragepy${x}" + + dotcoverage=$(find "$git_root" -name '.coverage' -or -name '.coverage.*' | head -1 || echo '') + if [ "$dotcoverage" != "" ]; + then + cd "$(dirname "$dotcoverage")" + if [ ! -f .coverage ]; + then + say " ${e}->${x} Running coverage combine" + coverage combine -a + fi + say " ${e}->${x} Running coverage xml" + if [ "$(coverage xml -i)" != "No data to report." ]; + then + files="$files +$PWD/coverage.xml" + else + say " ${r}No data to report.${x}" + fi + cd "$proj_root" + else + say " ${r}No .coverage file found.${x}" + fi + else + say "${e}==>${x} Python coveragepy not found" + fi + fi + else + say "${e}==>${x} Python coveragepy disabled" + fi + + if [ "$search_in_o" != "" ]; + then + # location override + search_in="$search_in_o" + fi + + say "$e==>$x Searching for coverage reports in:" + for _path in $search_in + do + say " ${g}+${x} $_path" + done + + patterns="find $search_in \( \ + -name vendor \ + -or -name '$bower_components' \ + -or -name '.egg-info*' \ + -or -name 'conftest_*.c.gcov' \ + -or -name .env \ + -or -name .envs \ + -or -name .git \ + -or -name .hg \ + -or -name .tox \ + -or -name .venv \ + -or -name .venvs \ + -or -name .virtualenv \ + -or -name .virtualenvs \ + -or -name .yarn-cache \ + -or -name __pycache__ \ + -or -name env \ + -or -name envs \ + -or -name htmlcov \ + -or -name js/generated/coverage \ + -or -name node_modules \ + -or -name venv \ + -or -name venvs \ + -or -name virtualenv \ + -or -name virtualenvs \ + \) -prune -or \ + -type f \( -name '*coverage*.*' \ + -or -name '*.clover' \ + -or -name '*.codecov.*' \ + -or -name '*.gcov' \ + -or -name '*.lcov' \ + -or -name '*.lst' \ + -or -name 'clover.xml' \ + -or -name 'cobertura.xml' \ + -or -name 'codecov.*' \ + -or -name 'cover.out' \ + -or -name 'codecov-result.json' \ + -or -name 'coverage-final.json' \ + -or -name 'excoveralls.json' \ + -or -name 'gcov.info' \ + -or -name 'jacoco*.xml' \ + -or -name '*Jacoco*.xml' \ + -or -name 'lcov.dat' \ + -or -name 'lcov.info' \ + -or -name 'luacov.report.out' \ + -or -name 'naxsi.info' \ + -or -name 'nosetests.xml' \ + -or -name 'report*.xml' \ + $include_cov \) \ + $exclude_cov \ + -not -name '*.am' \ + -not -name '*.bash' \ + -not -name '*.bat' \ + -not -name '*.bw' \ + -not -name '*.cfg' \ + -not -name '*.class' \ + -not -name '*.cmake' \ + -not -name '*.cmake' \ + -not -name '*.conf' \ + -not -name '*.coverage' \ + -not -name '*.cp' \ + -not -name '*.cpp' \ + -not -name '*.crt' \ + -not -name '*.css' \ + -not -name '*.csv' \ + -not -name '*.csv' \ + -not -name '*.data' \ + -not -name '*.db' \ + -not -name '*.dox' \ + -not -name '*.ec' \ + -not -name '*.ec' \ + -not -name '*.egg' \ + -not -name '*.el' \ + -not -name '*.env' \ + -not -name '*.erb' \ + -not -name '*.exe' \ + -not -name '*.ftl' \ + -not -name '*.gif' \ + -not -name '*.gradle' \ + -not -name '*.gz' \ + -not -name '*.h' \ + -not -name '*.html' \ + -not -name '*.in' \ + -not -name '*.jade' \ + -not -name '*.jar*' \ + -not -name '*.jpeg' \ + -not -name '*.jpg' \ + -not -name '*.js' \ + -not -name '*.less' \ + -not -name '*.log' \ + -not -name '*.m4' \ + -not -name '*.mak*' \ + -not -name '*.md' \ + -not -name '*.o' \ + -not -name '*.p12' \ + -not -name '*.pem' \ + -not -name '*.png' \ + -not -name '*.pom*' \ + -not -name '*.profdata' \ + -not -name '*.proto' \ + -not -name '*.ps1' \ + -not -name '*.pth' \ + -not -name '*.py' \ + -not -name '*.pyc' \ + -not -name '*.pyo' \ + -not -name '*.rb' \ + -not -name '*.rsp' \ + -not -name '*.rst' \ + -not -name '*.ru' \ + -not -name '*.sbt' \ + -not -name '*.scss' \ + -not -name '*.scss' \ + -not -name '*.serialized' \ + -not -name '*.sh' \ + -not -name '*.snapshot' \ + -not -name '*.sql' \ + -not -name '*.svg' \ + -not -name '*.tar.tz' \ + -not -name '*.template' \ + -not -name '*.whl' \ + -not -name '*.xcconfig' \ + -not -name '*.xcoverage.*' \ + -not -name '*/classycle/report.xml' \ + -not -name '*codecov.yml' \ + -not -name '*~' \ + -not -name '.*coveragerc' \ + -not -name '.coverage*' \ + -not -name 'coverage-summary.json' \ + -not -name 'createdFiles.lst' \ + -not -name 'fullLocaleNames.lst' \ + -not -name 'include.lst' \ + -not -name 'inputFiles.lst' \ + -not -name 'phpunit-code-coverage.xml' \ + -not -name 'phpunit-coverage.xml' \ + -not -name 'remapInstanbul.coverage*.json' \ + -not -name 'scoverage.measurements.*' \ + -not -name 'test_*_coverage.txt' \ + -not -name 'testrunner-coverage*' \ + -print 2>/dev/null" + files=$(eval "$patterns" || echo '') + +elif [ "$include_cov" != "" ]; +then + files=$(eval "find $search_in -type f \( ${include_cov:5} \)$exclude_cov 2>/dev/null" || echo '') +elif [ "$direct_file_upload" != "" ]; +then + files=$direct_file_upload +fi + +num_of_files=$(echo "$files" | wc -l | tr -d ' ') +if [ "$num_of_files" != '' ] && [ "$files" != '' ]; +then + say " ${e}->${x} Found $num_of_files reports" +fi + +# no files found +if [ "$files" = "" ]; +then + say "${r}-->${x} No coverage report found." + say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" + exit ${exit_with}; +fi + +if [ "$ft_network" == "1" ]; +then + say "${e}==>${x} Detecting git/mercurial file structure" + network=$(cd "$git_root" && git ls-files $git_ls_files_recurse_submodules_o 2>/dev/null || hg locate 2>/dev/null || echo "") + if [ "$network" = "" ]; + then + network=$(find "$git_root" \( \ + -name virtualenv \ + -name .virtualenv \ + -name virtualenvs \ + -name .virtualenvs \ + -name '*.png' \ + -name '*.gif' \ + -name '*.jpg' \ + -name '*.jpeg' \ + -name '*.md' \ + -name .env \ + -name .envs \ + -name env \ + -name envs \ + -name .venv \ + -name .venvs \ + -name venv \ + -name venvs \ + -name .git \ + -name .egg-info \ + -name shunit2-2.1.6 \ + -name vendor \ + -name __pycache__ \ + -name node_modules \ + -path "*/$bower_components/*" \ + -path '*/target/delombok/*' \ + -path '*/build/lib/*' \ + -path '*/js/generated/coverage/*' \ + \) -prune -or \ + -type f -print 2>/dev/null || echo '') + fi + + if [ "$network_filter_o" != "" ]; + then + network=$(echo "$network" | grep -e "$network_filter_o/*") + fi + if [ "$prefix_o" != "" ]; + then + network=$(echo "$network" | awk "{print \"$prefix_o/\"\$0}") + fi +fi + +upload_file=$(mktemp /tmp/codecov.XXXXXX) +adjustments_file=$(mktemp /tmp/codecov.adjustments.XXXXXX) + +cleanup() { + rm -f "$upload_file" "$adjustments_file" "$upload_file.gz" +} + +trap cleanup INT ABRT TERM + + +if [ "$env" != "" ]; +then + inc_env="" + say "${e}==>${x} Appending build variables" + for varname in $(echo "$env" | tr ',' ' ') + do + if [ "$varname" != "" ]; + then + say " ${g}+${x} $varname" + inc_env="${inc_env}${varname}=$(eval echo "\$${varname}") +" + fi + done + echo "$inc_env<<<<<< ENV" >> "$upload_file" +fi + +# Append git file list +# write discovered yaml location +if [ "$direct_file_upload" = "" ]; +then + echo "$yaml" >> "$upload_file" +fi + +if [ "$ft_network" == "1" ]; +then + i="woff|eot|otf" # fonts + i="$i|gif|png|jpg|jpeg|psd" # images + i="$i|ptt|pptx|numbers|pages|md|txt|xlsx|docx|doc|pdf|csv" # docs + i="$i|.gitignore" # supporting docs + + if [ "$ft_html" != "1" ]; + then + i="$i|html" + fi + + if [ "$ft_yaml" != "1" ]; + then + i="$i|yml|yaml" + fi + + echo "$network" | grep -vwE "($i)$" >> "$upload_file" +fi +echo "<<<<<< network" >> "$upload_file" + +if [ "$direct_file_upload" = "" ]; +then + fr=0 + say "${e}==>${x} Reading reports" + while IFS='' read -r file; + do + # read the coverage file + if [ "$(echo "$file" | tr -d ' ')" != '' ]; + then + if [ -f "$file" ]; + then + report_len=$(wc -c < "$file") + if [ "$report_len" -ne 0 ]; + then + say " ${g}+${x} $file ${e}bytes=$(echo "$report_len" | tr -d ' ')${x}" + # append to to upload + _filename=$(basename "$file") + if [ "${_filename##*.}" = 'gcov' ]; + then + { + echo "# path=$(echo "$file.reduced" | sed "s|^$git_root/||")"; + # get file name + head -1 "$file"; + } >> "$upload_file" + # 1. remove source code + # 2. remove ending bracket lines + # 3. remove whitespace + # 4. remove contextual lines + # 5. remove function names + awk -F': *' '{print $1":"$2":"}' "$file" \ + | sed '\/: *} *$/d' \ + | sed 's/^ *//' \ + | sed '/^-/d' \ + | sed 's/^function.*/func/' >> "$upload_file" + else + { + echo "# path=${file//^$git_root/||}"; + cat "$file"; + } >> "$upload_file" + fi + echo "<<<<<< EOF" >> "$upload_file" + fr=1 + if [ "$clean" = "1" ]; + then + rm "$file" + fi + else + say " ${r}-${x} Skipping empty file $file" + fi + else + say " ${r}-${x} file not found at $file" + fi + fi + done <<< "$(echo -e "$files")" + + if [ "$fr" = "0" ]; + then + say "${r}-->${x} No coverage data found." + say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" + say " search for your projects language to learn how to collect reports." + exit ${exit_with}; + fi +else + cp "$direct_file_upload" "$upload_file" + if [ "$clean" = "1" ]; + then + rm "$direct_file_upload" + fi +fi + +if [ "$ft_fix" = "1" ]; +then + say "${e}==>${x} Appending adjustments" + say " ${b}https://docs.codecov.io/docs/fixing-reports${x}" + + empty_line='^[[:space:]]*$' + # // + syntax_comment='^[[:space:]]*//.*' + # /* or */ + syntax_comment_block='^[[:space:]]*(\/\*|\*\/)[[:space:]]*$' + # { or } + syntax_bracket='^[[:space:]]*[\{\}][[:space:]]*(//.*)?$' + # [ or ] + syntax_list='^[[:space:]]*[][][[:space:]]*(//.*)?$' + # func ... { + syntax_go_func='^[[:space:]]*[func].*[\{][[:space:]]*$' + + # shellcheck disable=SC2089 + skip_dirs="-not -path '*/$bower_components/*' \ + -not -path '*/node_modules/*'" + + cut_and_join() { + awk 'BEGIN { FS=":" } + $3 ~ /\/\*/ || $3 ~ /\*\// { print $0 ; next } + $1!=key { if (key!="") print out ; key=$1 ; out=$1":"$2 ; next } + { out=out","$2 } + END { print out }' 2>/dev/null + } + + if echo "$network" | grep -m1 '.kt$' 1>/dev/null; + then + # skip brackets and comments + cd "$git_root" && \ + find . -type f \ + -name '*.kt' \ + -exec \ + grep -nIHE -e "$syntax_bracket" \ + -e "$syntax_comment_block" {} \; \ + | cut_and_join \ + >> "$adjustments_file" \ + || echo '' + + # last line in file + cd "$git_root" && \ + find . -type f \ + -name '*.kt' -exec \ + wc -l {} \; \ + | while read -r l; do echo "EOF: $l"; done \ + 2>/dev/null \ + >> "$adjustments_file" \ + || echo '' + fi + + if echo "$network" | grep -m1 '.go$' 1>/dev/null; + then + # skip empty lines, comments, and brackets + cd "$git_root" && \ + find . -type f \ + -not -path '*/vendor/*' \ + -not -path '*/caches/*' \ + -name '*.go' \ + -exec \ + grep -nIHE \ + -e "$empty_line" \ + -e "$syntax_comment" \ + -e "$syntax_comment_block" \ + -e "$syntax_bracket" \ + -e "$syntax_go_func" \ + {} \; \ + | cut_and_join \ + >> "$adjustments_file" \ + || echo '' + fi + + if echo "$network" | grep -m1 '.dart$' 1>/dev/null; + then + # skip brackets + cd "$git_root" && \ + find . -type f \ + -name '*.dart' \ + -exec \ + grep -nIHE \ + -e "$syntax_bracket" \ + {} \; \ + | cut_and_join \ + >> "$adjustments_file" \ + || echo '' + fi + + if echo "$network" | grep -m1 '.php$' 1>/dev/null; + then + # skip empty lines, comments, and brackets + cd "$git_root" && \ + find . -type f \ + -not -path "*/vendor/*" \ + -name '*.php' \ + -exec \ + grep -nIHE \ + -e "$syntax_list" \ + -e "$syntax_bracket" \ + -e '^[[:space:]]*\);[[:space:]]*(//.*)?$' \ + {} \; \ + | cut_and_join \ + >> "$adjustments_file" \ + || echo '' + fi + + if echo "$network" | grep -m1 '\(.c\.cpp\|.cxx\|.h\|.hpp\|.m\|.swift\|.vala\)$' 1>/dev/null; + then + # skip brackets + # shellcheck disable=SC2086,SC2090 + cd "$git_root" && \ + find . -type f \ + $skip_dirs \ + \( \ + -name '*.c' \ + -or -name '*.cpp' \ + -or -name '*.cxx' \ + -or -name '*.h' \ + -or -name '*.hpp' \ + -or -name '*.m' \ + -or -name '*.swift' \ + -or -name '*.vala' \ + \) -exec \ + grep -nIHE \ + -e "$empty_line" \ + -e "$syntax_bracket" \ + -e '// LCOV_EXCL' \ + {} \; \ + | cut_and_join \ + >> "$adjustments_file" \ + || echo '' + + # skip brackets + # shellcheck disable=SC2086,SC2090 + cd "$git_root" && \ + find . -type f \ + $skip_dirs \ + \( \ + -name '*.c' \ + -or -name '*.cpp' \ + -or -name '*.cxx' \ + -or -name '*.h' \ + -or -name '*.hpp' \ + -or -name '*.m' \ + -or -name '*.swift' \ + -or -name '*.vala' \ + \) -exec \ + grep -nIH '// LCOV_EXCL' \ + {} \; \ + >> "$adjustments_file" \ + || echo '' + + fi + + found=$(< "$adjustments_file" tr -d ' ') + + if [ "$found" != "" ]; + then + say " ${g}+${x} Found adjustments" + { + echo "# path=fixes"; + cat "$adjustments_file"; + echo "<<<<<< EOF"; + } >> "$upload_file" + rm -rf "$adjustments_file" + else + say " ${e}->${x} No adjustments found" + fi +fi + +if [ "$url_o" != "" ]; +then + url="$url_o" +fi + +if [ "$dump" != "0" ]; +then + # trim whitespace from query + say " ${e}->${x} Dumping upload file (no upload)" + echo "$url/upload/v4?$(echo "package=$package-$VERSION&token=$token&$query" | tr -d ' ')" + cat "$upload_file" +else + if [ "$save_to" != "" ]; + then + say "${e}==>${x} Copying upload file to ${save_to}" + mkdir -p "$(dirname "$save_to")" + cp "$upload_file" "$save_to" + fi + + say "${e}==>${x} Gzipping contents" + gzip -nf9 "$upload_file" + say " $(du -h "$upload_file.gz")" + + query=$(echo "${query}" | tr -d ' ') + say "${e}==>${x} Uploading reports" + say " ${e}url:${x} $url" + say " ${e}query:${x} $query" + + # Full query without token (to display on terminal output) + queryNoToken=$(echo "package=$package-$VERSION&token=secret&$query" | tr -d ' ') + # now add token to query + query=$(echo "package=$package-$VERSION&token=$token&$query" | tr -d ' ') + + if [ "$ft_s3" = "1" ]; + then + say "${e}->${x} Pinging Codecov" + say "$url/upload/v4?$queryNoToken" + # shellcheck disable=SC2086,2090 + res=$(curl $curl_s -X POST $cacert \ + --retry 5 --retry-delay 2 --connect-timeout 2 \ + -H 'X-Reduced-Redundancy: false' \ + -H 'X-Content-Type: application/x-gzip' \ + -H 'Content-Length: 0' \ + --write-out "\n%{response_code}\n" \ + $curlargs \ + "$url/upload/v4?$query" || true) + # a good reply is "https://codecov.io/" + "\n" + "https://storage.googleapis.com/codecov/..." + s3target=$(echo "$res" | sed -n 2p) + status=$(tail -n1 <<< "$res") + + if [ "$status" = "200" ] && [ "$s3target" != "" ]; + then + say "${e}->${x} Uploading to" + say "${s3target}" + + # shellcheck disable=SC2086 + s3=$(curl -fiX PUT \ + --data-binary @"$upload_file.gz" \ + -H 'Content-Type: application/x-gzip' \ + -H 'Content-Encoding: gzip' \ + $curlawsargs \ + "$s3target" || true) + + if [ "$s3" != "" ]; + then + say " ${g}->${x} Reports have been successfully queued for processing at ${b}$(echo "$res" | sed -n 1p)${x}" + exit 0 + else + say " ${r}X>${x} Failed to upload" + fi + elif [ "$status" = "400" ]; + then + # 400 Error + say "${r}${res}${x}" + exit ${exit_with} + else + say "${r}${res}${x}" + fi + fi + + say "${e}==>${x} Uploading to Codecov" + + # shellcheck disable=SC2086,2090 + res=$(curl -X POST $cacert \ + --data-binary @"$upload_file.gz" \ + --retry 5 --retry-delay 2 --connect-timeout 2 \ + -H 'Content-Type: text/plain' \ + -H 'Content-Encoding: gzip' \ + -H 'X-Content-Encoding: gzip' \ + -H 'Accept: text/plain' \ + $curlargs \ + "$url/upload/v2?$query&attempt=$i" || echo 'HTTP 500') + # HTTP 200 + # http://.... + status=$(echo "$res" | head -1 | cut -d' ' -f2) + if [ "$status" = "" ] || [ "$status" = "200" ]; + then + say " Reports have been successfully queued for processing at ${b}$(echo "$res" | head -2 | tail -1)${x}" + exit 0 + else + say " ${g}${res}${x}" + exit ${exit_with} + fi + + say " ${r}X> Failed to upload coverage reports${x}" +fi + +exit ${exit_with} diff --git a/ci/scripts/dogfood.py b/ci/scripts/dogfood.py new file mode 100755 index 0000000000..79445bf18a --- /dev/null +++ b/ci/scripts/dogfood.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-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-Present Datadog, Inc + +import re +import subprocess +import sys +import os +from argparse import ArgumentParser, Namespace +from tempfile import TemporaryDirectory +from typing import Tuple +from xmlrpc.client import Boolean + +import requests +from git import Repo + +TARGET_APP = "app" +TARGET_DEMO = "demo" +TARGET_GRADLE_PLUGIN = "gradle-plugin" + +REPOSITORIES = { + TARGET_APP: "datadog-android", + TARGET_DEMO: "shopist-android", + TARGET_GRADLE_PLUGIN: "dd-sdk-android-gradle-plugin", + # Flutter is not needed because it pulls updates instead of being pushed them with the dogfood script. +} + +FILE_PATH = { + TARGET_APP: os.path.join("gradle", "libs.versions.toml"), + TARGET_DEMO: os.path.join("gradle", "libs.versions.toml"), + TARGET_GRADLE_PLUGIN: os.path.join("gradle", "libs.versions.toml"), +} + +PREFIX = { + TARGET_APP: "datadog", + TARGET_DEMO: "datadogSdk", + TARGET_GRADLE_PLUGIN: "datadogSdk", +} + + +def parse_arguments(args: list) -> Namespace: + parser = ArgumentParser() + + parser.add_argument("-v", "--version", required=True, help="the version of the SDK") + parser.add_argument("-t", "--target", required=True, + choices=[TARGET_APP, TARGET_DEMO, TARGET_GRADLE_PLUGIN], + help="the target repository") + parser.add_argument("-d", "--dry-run", required=False, dest="dry_run", + help="Don't push changes or create a PR.", action="http://github.com/store_true") + + return parser.parse_args(args) + + +def github_create_pr(repository: str, branch_name: str, base_name: str, version: str, previous_version: str, gh_token: str) -> int: + headers = { + 'authorization': "Bearer " + gh_token, + 'Accept': 'application/vnd.github.v3+json', + } + body = "This PR has been created automatically by the CI" + if previous_version: + diff = "Updating Datadog SDK from version {previous_version} to version {version}: [diff](https://github.com/DataDog/dd-sdk-android/compare/{previous_version}...{version})".format(previous_version=previous_version, version=version) + body = "\\n".join([body, diff]) + data = '{"body": "' + body + '", ' \ + '"title": "Update Datadog SDK to version ' + version + '", ' \ + '"base":"' + base_name + '", "head":"' + branch_name + '"}' + + url = "https://api.github.com/repos/DataDog/" + repository + "/pulls" + response = requests.post(url=url, headers=headers, data=data) + if response.status_code == 201: + print("Pull Request created successfully") + return 0 + else: + print("pull request failed " + str(response.status_code) + '\n' + response.text) + return 1 + +def generate_target_code(target: str, temp_dir_path: str, version: str): + print("Generating code with version " + version) + file_path = FILE_PATH[target] + target_file = os.path.join(temp_dir_path, file_path) + prefix = PREFIX[target] + regex = prefix + " = \"[0-9a-z\\.-]+\"" + + previous_version = None + + with open(target_file, 'r') as target: + content = target.read() + previous_version_search = re.search(prefix + " = \"(.*)\"", content, flags=re.M) + if previous_version_search: + previous_version = previous_version_search.group(1) + updated_content = re.sub(regex, prefix + " = \"" + version + "\"", content, flags=re.M) + + with open(target_file, 'w') as target: + target.write(updated_content) + + return previous_version + + +def git_clone_repository(repo_name: str, gh_token: str, temp_dir_path: str) -> Tuple[Repo, str]: + print("Cloning repository " + repo_name) + url = "https://x-access-token/" + gh_token + "@github.com/DataDog/" + repo_name + repo = Repo.clone_from(url, temp_dir_path) + base_name = repo.active_branch.name + return repo, base_name + + +def git_push_changes(repo: Repo, version: str): + print("Committing changes") + repo.git.add(update=True) + repo.index.commit("Update Datadog SDK to " + version) + + print("Pushing branch") + origin = repo.remote(name="origin") + repo.git.push("--set-upstream", "--force", origin, repo.head.ref) + + +def update_dependant(version: str, target: str, gh_token: str, dry_run: bool) -> int: + branch_name = "update_sdk_" + version + temp_dir = TemporaryDirectory() + temp_dir_path = temp_dir.name + repo_name = REPOSITORIES[target] + + repo, base_name = git_clone_repository(repo_name, gh_token, temp_dir_path) + + print("Creating branch " + branch_name) + repo.git.checkout('HEAD', b=branch_name) + + previous_version = generate_target_code(target, temp_dir_path, version) + + if not repo.is_dirty(): + print("Nothing to commit, all is in order-") + return 0 + + if not dry_run: + git_push_changes(repo, version) + + return github_create_pr(repo_name, branch_name, base_name, version, previous_version, gh_token) + + return 0 + +def run_main() -> int: + cli_args = parse_arguments(sys.argv[1:]) + + gh_token = os.getenv("GITHUB_TOKEN") + + return update_dependant(cli_args.version, cli_args.target, gh_token, cli_args.dry_run) + + +if __name__ == "__main__": + sys.exit(run_main()) diff --git a/ci/scripts/merge_verification_metadata.py b/ci/scripts/merge_verification_metadata.py new file mode 100755 index 0000000000..724f197e98 --- /dev/null +++ b/ci/scripts/merge_verification_metadata.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-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-Present Datadog, Inc + +import sys +import os +import glob + +import xml.etree.ElementTree as ET +import xml.dom.minidom as MD + +from typing import List + + +DEFAULT_NS = "https://schema.gradle.org/dependency-verification" +NS_PREFIX = "{%s}" % DEFAULT_NS +NSMAP = {None : DEFAULT_NS} + +ROOT_TAG = (NS_PREFIX + "verification-metadata") +CONFIGURATION_TAG = (NS_PREFIX + "configuration") +METADATA_TAG = (NS_PREFIX + "verify-metadata") +SIGNATURES_TAG = (NS_PREFIX + "verify-signatures") + +COMPONENTS_TAG = (NS_PREFIX + "components") +COMPONENT_TAG = (NS_PREFIX + "component") + +def run_main() -> int: + files = glob.glob('**/verification-metadata.xml', recursive=True) + + # prepare final xml + root = ET.Element(ROOT_TAG) + configuration = ET.Element(CONFIGURATION_TAG) + metadata = ET.Element(METADATA_TAG) + metadata.text = "true" + configuration.insert(0, metadata) + signatures = ET.Element(SIGNATURES_TAG) + signatures.text = "true" + configuration.insert(1, signatures) + root.insert(0, configuration) + + components = ET.Element(COMPONENTS_TAG) + root.insert(1, components) + + index = 0 + for filename in files: + data = ET.parse(filename).getroot() + for child in data: + if child.tag == COMPONENTS_TAG: + for component in child: + components.insert(index, component) + index = index + 1 + + # remove unnecessary whitespaces in content + for elem in root.iter(): + if elem.text is not None: + elem.text = elem.text.strip() + if elem.tail is not None: + elem.tail = elem.tail.strip() + + raw_xml = ET.tostring(root, method="xml").decode('utf-8') + # A bug in the python etree xml api prevents writing standalone xml, so we need + # to remove the default namespace: + sanitized_xml = raw_xml.replace("" - implementation "com.datadoghq:dd-sdk-android-coil:" -} -``` - -### Initial setup - -Before using the SDK, set up the library with your application -context, client token, and application ID. -To generate a client token and an application ID, check **UX Monitoring > RUM Applications > New Application** -in the Datadog dashboard. - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "").build() - Datadog.initialize(this, config) - - val monitor = RumMonitor.Builder().build() - GlobalRum.registerIfAbsent(monitor) - } -} -``` - -Follow Coil's [API documentation][2] to: - - - Create your own `ImageLoader` by providing your own OkHttpClient (configured with `DatadogInterceptor`). - -```kotlin - val imageLoader = ImageLoader.Builder(context).okHttpClient(okHttpClient).build() - Coil.setImageLoader(imageLoader) -``` - -- Decorate the `ImageRequest.Builder` with the `DatadogCoilRequestListener` whenever you perform an image loading request. - - ```kotlin - imageView.load(uri){ - listener(DatadogCoilRequestListener()) - } - ``` - -This automatically tracks Coil's network requests (creating both APM Traces and RUM Resource events), and listens for disk cache errors (creating RUM Error events). - -## Contributing - -For details on contributing, read the -[Contributing Guide](../CONTRIBUTING.md). - -## License - -[Apache License, v2.0](../LICENSE) - -[1]: https://github.com/coil-kt/coil -[2]: https://coil-kt.github.io/coil/getting_started/ diff --git a/dd-sdk-android-coil/apiSurface b/dd-sdk-android-coil/apiSurface deleted file mode 100644 index 8072eef9dc..0000000000 --- a/dd-sdk-android-coil/apiSurface +++ /dev/null @@ -1,3 +0,0 @@ -class com.datadog.android.coil.DatadogCoilRequestListener : coil.request.ImageRequest.Listener - override fun onError(coil.request.ImageRequest, Throwable) - companion object diff --git a/dd-sdk-android-coil/build.gradle.kts b/dd-sdk-android-coil/build.gradle.kts deleted file mode 100644 index 1b6bf7cc65..0000000000 --- a/dd-sdk-android-coil/build.gradle.kts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-2019 Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.implementation -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - id("androidx.benchmark") - kotlin("android") - kotlin("android.extensions") - kotlin("kapt") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - } -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.OkHttp) - implementation(Dependencies.Libraries.Coil) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-coil/src/main/AndroidManifest.xml b/dd-sdk-android-coil/src/main/AndroidManifest.xml deleted file mode 100644 index fb5801d311..0000000000 --- a/dd-sdk-android-coil/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/dd-sdk-android-coil/src/main/kotlin/com/datadog/android/coil/DatadogCoilRequestListener.kt b/dd-sdk-android-coil/src/main/kotlin/com/datadog/android/coil/DatadogCoilRequestListener.kt deleted file mode 100644 index befad0b2a2..0000000000 --- a/dd-sdk-android-coil/src/main/kotlin/com/datadog/android/coil/DatadogCoilRequestListener.kt +++ /dev/null @@ -1,75 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.coil - -import android.net.Uri -import coil.request.ImageRequest -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import java.io.File -import okhttp3.HttpUrl - -/** - * Provides an implementation of [coil.request.ImageRequest.Listener] already set up to send relevant information - * to Datadog. - * - * It will automatically send RUM error events whenever a Coil [ImageRequest] - * throws any [Exception]. - */ -class DatadogCoilRequestListener : ImageRequest.Listener { - - // region Listener - - /** @inheritDoc */ - override fun onError(request: ImageRequest, throwable: Throwable) { - GlobalRum.get().addError( - REQUEST_ERROR_MESSAGE, - RumErrorSource.SOURCE, - throwable, - extractRequestAttributes(request) - ) - } - - // endregion - - // region Internals - - private fun extractRequestAttributes(request: ImageRequest): Map { - return when (request.data) { - is String -> { - mapOf( - REQUEST_PATH_TAG to request.data as String - ) - } - is Uri -> { - mapOf( - REQUEST_PATH_TAG to (request.data as Uri).path - ) - } - is HttpUrl -> { - mapOf( - REQUEST_PATH_TAG to (request.data as HttpUrl).url().toString() - ) - } - is File -> { - mapOf( - REQUEST_PATH_TAG to (request.data as File).path - ) - } - else -> { - emptyMap() - } - } - } - - // endregion - - companion object { - internal const val REQUEST_ERROR_MESSAGE = "Coil request error" - internal const val REQUEST_PATH_TAG = "request_path" - } -} diff --git a/dd-sdk-android-coil/src/test/kotlin/com/datadog/android/coil/utils/Configurator.kt b/dd-sdk-android-coil/src/test/kotlin/com/datadog/android/coil/utils/Configurator.kt deleted file mode 100644 index 2fabe3eece..0000000000 --- a/dd-sdk-android-coil/src/test/kotlin/com/datadog/android/coil/utils/Configurator.kt +++ /dev/null @@ -1,19 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.coil.utils - -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeConfigurator -import fr.xgouchet.elmyr.jvm.useJvmFactories - -internal class Configurator : - ForgeConfigurator { - override fun configure(forge: Forge) { - forge.addFactory(ThrowableForgeryFactory()) - forge.useJvmFactories() - } -} diff --git a/dd-sdk-android-coil/src/test/kotlin/com/datadog/android/coil/utils/ThrowableForgeryFactory.kt b/dd-sdk-android-coil/src/test/kotlin/com/datadog/android/coil/utils/ThrowableForgeryFactory.kt deleted file mode 100644 index 9f8ba6527e..0000000000 --- a/dd-sdk-android-coil/src/test/kotlin/com/datadog/android/coil/utils/ThrowableForgeryFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.datadog.android.coil.utils - -import com.datadog.tools.unit.forge.aThrowable -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeryFactory - -class ThrowableForgeryFactory : - ForgeryFactory { - override fun getForgery(forge: Forge): Throwable { - return forge.aThrowable() - } -} diff --git a/dd-sdk-android-coil/transitiveDependencies b/dd-sdk-android-coil/transitiveDependencies deleted file mode 100644 index 2283435255..0000000000 --- a/dd-sdk-android-coil/transitiveDependencies +++ /dev/null @@ -1,18 +0,0 @@ -Dependencies List - -androidx.annotation:annotation:1.1.0 : 27 Kb -androidx.lifecycle:lifecycle-common-java8:2.2.0 : 1015 b -androidx.lifecycle:lifecycle-common:2.2.0 : 21 Kb -com.squareup.okhttp3:okhttp:3.12.12 : 417 Kb -com.squareup.okio:okio:2.9.0 : 247 Kb -io.coil-kt:coil-base:1.0.0 : 395 Kb -io.coil-kt:coil:1.0.0 : 15 Kb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.4.10 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 : 19 Kb -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.3.9 : 1629 Kb -org.jetbrains:annotations:13.0 : 17 Kb - -Total transitive dependencies size : 4 Mb - diff --git a/dd-sdk-android-core/.gitignore b/dd-sdk-android-core/.gitignore new file mode 100644 index 0000000000..a26303a77f --- /dev/null +++ b/dd-sdk-android-core/.gitignore @@ -0,0 +1,25 @@ +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +build/ + +# Generated Poko +src/main/kotlin/com/datadog/android/rum/model/ +src/main/kotlin/com/datadog/android/telemetry/model/ +src/main/kotlin/com/datadog/android/core/model/ +src/main/kotlin/com/datadog/android/tracing/model/ +src/main/kotlin/com/datadog/android/log/model/ \ No newline at end of file diff --git a/dd-sdk-android-core/BackgroundThreads.md b/dd-sdk-android-core/BackgroundThreads.md new file mode 100644 index 0000000000..fd9886a1c4 --- /dev/null +++ b/dd-sdk-android-core/BackgroundThreads.md @@ -0,0 +1,104 @@ +# Background Threads + +This document lists all background threads created by the Datadog SDK (and its features and integrations), +with their purposes, where they are created, and the stress testing done on each of them. + +### `CoreFeature.persistenceExecutorService`: `FlushableExecutorService` + +##### Purpose and Usage + +This instance (one per `CoreFeature`) is used to treat all data writing process. +It is specifically used for: + +- (internal) initialize the NTP process on startup +- (internal) used in the `ConsentAwareStorage` to process the `writeCurrentBatch()` calls +- (external) used in the `AbstractStorage` to process the `writeCurrentBatch()` calls +- (internal) used in the `ConsentAwareFileOrchestrator` to migrate files when tracking consent changes +- (internal) used in the `DatadogExceptionHandler` to ensure the crash is written to disk before letting the process + exit + +##### Load and scalability + +##### Implementation details + +Except for our unit tests the actual implementation is our own `BackPressureExecutorService`. + +--- + +### `CoreFeature.uploadExecutorService`: `ScheduledThreadPoolExecutor` + +##### Purpose and Usage + +This instance (one per `CoreFeature`) is used to process the data upload process. +It is specifically used for: + +- (internal) creates the initial Configuration Telemetry on `DatadogCore` startup + (delayed to allow cross platform SDKs to update the configuration) +- (internal) used in the `DataUploadScheduler` to schedule regular upload cycles + +##### Load and scalability + +##### Implementation details + +Except for our unit tests, the actual implementation is our own `LoggingScheduledThreadPoolExecutor`. + +--- + +### RUM View Tracking Strategies : `ScheduledExecutorService` + +##### Purpose and Usage + +This instance (one in our basic Android lifecycle based VTS, i.e.: Activity, Legacy Fragment and Oreo+ Fragment) +is used to delay the call to `RumMonitor.stopView()`. This is linked with ticket `RUM-616` to avoid gaps in the +session coverage. (cf : [Github PR #1578](https://github.com/DataDog/dd-sdk-android/pull/1578)). + +##### Load and scalability + +##### Implementation details + +Except for our unit tests, the actual implementation is our own `LoggingScheduledThreadPoolExecutor`. + +--- + +### `RumFeature.vitalExecutorService`: `ScheduledExecutorService` + +##### Purpose and Usage + +This instance (one per `RumFeature`) is used to schedule regular readings of the system vitals, namely the CPU and +memory usage. + +##### Load and scalability + +##### Implementation details + +Except for our unit tests, the actual implementation is our own `LoggingScheduledThreadPoolExecutor`. + +--- + +### `RumFeature.anrDetectorExecutorService: `ExecutorService` + +##### Purpose and Usage + +This instance (one per `RumFeature`) is used for our own API < 30 ANR detection mechanism. + +##### Load and scalability + +##### Implementation details + +Except for our unit tests, the actual implementation is our own `BackPressureExecutorService`. + +--- + +### `DatadogRumMonitor.executorService`: `ExecutorService` + +##### Purpose and Usage + +This instance (one per `DatadogRumMonitor`) is used to defer the process of raw events (usually captured from the +main thread) and eventually generate actual Json event that needs to be written on disk. + +##### Load and scalability + +##### Implementation details + +Except for our unit tests, the actual implementation is our own `BackPressureExecutorService`. + diff --git a/dd-sdk-android-core/README.md b/dd-sdk-android-core/README.md new file mode 100644 index 0000000000..19ec186edf --- /dev/null +++ b/dd-sdk-android-core/README.md @@ -0,0 +1,89 @@ +# Datadog SDK for Android - core library + +## Getting started + +To include the Datadog SDK for Android in your project, simply add any product you want to use to your application's `build.gradle` file. + +For example, in case of RUM: + +```groovy +dependencies { + implementation "com.datadoghq:dd-sdk-android-rum:" +} +``` + +### Initial Setup + +Before you can use the SDK, you need to setup the library with your application +context and your API token. You can create a token from the Integrations > API +in Datadog. **Make sure you create a key of type `Client Token`.** + +```kotlin +class SampleApplication : Application() { + override fun onCreate() { + super.onCreate() + val configuration = Configuration.Builder( + clientToken = CLIENT_TOKEN, + env = ENV_NAME, + variant = APP_VARIANT_NAME + ) + .useSite(DatadogSite.US1) // replace with the site you're targeting (e.g.: US3, EU1, …) + .build() + Datadog.initialize(this, configuration, trackingConsent) + } +} +``` + +### Using a secondary instance of the SDK + +It is possible to initialize multiple instances of the SDK by associating them with a name. Many methods of the SDK can optionally take an SDK instance as an argument. If not provided, the call is associated with the default (nameless) SDK instance. + +Here is an example illustrating how to initialize a secondary core instance and use it: + +```kotlin +val namedSdkInstance = Datadog.initialize("myInstance", context, configuration, trackingConsent) +val userInfo = UserInfo(...) +Datadog.setUserInfo(userInfo, sdkCore = namedSdkInstance) +``` + +**Note**: The SDK instance name should have the same value between application runs. Storage paths for SDK events are associated with it. + +You can retrieve the named SDK instance by calling `Datadog.getInstance()` and use the `Datadog.isInitialized()` method to check if the particular SDK instance is initialized. + +## Setting up Datadog RUM SDK + +See the dedicated [Datadog Android RUM Collection documentation][1] to learn how to send RUM data from your Android or Android TV application to Datadog. + +## Setting up the Datadog Logs SDK + +See the dedicated [Datadog Android Log Collection documentation][2] to learn how to forward logs from your Android or Android TV application to Datadog. + +## Setting up Datadog Trace SDK + +See the dedicated [Datadog Android Trace Collection documentation][3] to learn how to send traces from your Android or Android TV application to Datadog. + +## Setting the Library's verbosity + +If you need to get information about the Library, you can set the verbosity +level as follows: + +```kotlin + Datadog.setVerbosity(Log.INFO) +``` + +All the internal messages in the library with a priority equal or higher than +the provided level will be logged to Android's LogCat. + +## Contributing + +Pull requests are welcome, but please open an issue first 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://docs.datadoghq.com/real_user_monitoring/android/?tab=kotlin +[2]: https://docs.datadoghq.com/logs/log_collection/android/?tab=kotlin +[3]: https://docs.datadoghq.com/tracing/trace_collection/dd_libraries/android/?tab=kotlin diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface new file mode 100644 index 0000000000..0861a04ea8 --- /dev/null +++ b/dd-sdk-android-core/api/apiSurface @@ -0,0 +1,443 @@ +object com.datadog.android.Datadog + fun initialize(String?, android.content.Context, com.datadog.android.core.configuration.Configuration, com.datadog.android.privacy.TrackingConsent): com.datadog.android.api.SdkCore? + fun initialize(android.content.Context, com.datadog.android.core.configuration.Configuration, com.datadog.android.privacy.TrackingConsent): com.datadog.android.api.SdkCore? + fun getInstance(String? = null): com.datadog.android.api.SdkCore + fun isInitialized(String? = null): Boolean + fun stopInstance(String? = null) + fun setVerbosity(Int) + fun getVerbosity(): Int + fun setTrackingConsent(com.datadog.android.privacy.TrackingConsent, com.datadog.android.api.SdkCore = getInstance()) + fun setUserInfo(String, String? = null, String? = null, Map = emptyMap(), com.datadog.android.api.SdkCore = getInstance()) + fun addUserProperties(Map, com.datadog.android.api.SdkCore = getInstance()) + fun clearUserInfo(com.datadog.android.api.SdkCore = getInstance()) + fun clearAllData(com.datadog.android.api.SdkCore = getInstance()) + fun setAccountInfo(String, String? = null, Map = emptyMap(), com.datadog.android.api.SdkCore = getInstance()) + fun addAccountExtraInfo(Map, com.datadog.android.api.SdkCore = getInstance()) + fun clearAccountInfo(com.datadog.android.api.SdkCore = getInstance()) + fun _internalProxy(String? = null): _InternalProxy +enum com.datadog.android.DatadogSite + - US1 + - US3 + - US5 + - EU1 + - AP1 + - AP2 + - US1_FED + - STAGING + val intakeEndpoint: String +class com.datadog.android._InternalProxy + class _TelemetryProxy + fun debug(String) + fun error(String, Throwable? = null) + fun error(String, String?, String?) + val _telemetry: _TelemetryProxy + fun setCustomAppVersion(String) + companion object + fun allowClearTextHttp(com.datadog.android.core.configuration.Configuration.Builder): com.datadog.android.core.configuration.Configuration.Builder +interface com.datadog.android.api.InternalLogger + enum Level + - VERBOSE + - DEBUG + - INFO + - WARN + - ERROR + enum Target + - USER + - MAINTAINER + - TELEMETRY + fun log(Level, Target, () -> String, Throwable? = null, Boolean = false, Map? = null) + fun log(Level, List, () -> String, Throwable? = null, Boolean = false, Map? = null) + fun logMetric(() -> String, Map, Float, Float? = null) + fun startPerformanceMeasure(String, com.datadog.android.core.metrics.TelemetryMetricType, Float, String): com.datadog.android.core.metrics.PerformanceMetric? + fun logApiUsage(Float = DEFAULT_API_USAGE_TELEMETRY_SAMPLING_RATE, () -> com.datadog.android.internal.telemetry.InternalTelemetryEvent.ApiUsage) + companion object + val UNBOUND: InternalLogger +interface com.datadog.android.api.SdkCore + val name: String + val time: com.datadog.android.api.context.TimeInfo + val service: String + fun isCoreActive(): Boolean + fun setTrackingConsent(com.datadog.android.privacy.TrackingConsent) + fun setUserInfo(String, String? = null, String? = null, Map = emptyMap()) + fun addUserProperties(Map) + fun clearUserInfo() + fun clearAllData() + fun setAccountInfo(String, String? = null, Map = emptyMap()) + fun addAccountExtraInfo(Map) + fun clearAccountInfo() +data class com.datadog.android.api.context.AccountInfo + constructor(String, String? = null, Map = emptyMap()) +data class com.datadog.android.api.context.DatadogContext + constructor(com.datadog.android.DatadogSite, String, String, String, String, String, String, String, TimeInfo, ProcessInfo, NetworkInfo, DeviceInfo, UserInfo, AccountInfo?, com.datadog.android.privacy.TrackingConsent, String?, Map>) +data class com.datadog.android.api.context.DeviceInfo + constructor(String, String, String, DeviceType, String, String, String, String, String, Int?, LocaleInfo) +enum com.datadog.android.api.context.DeviceType + - MOBILE + - TABLET + - TV + - DESKTOP + - GAMING_CONSOLE + - BOT + - OTHER +data class com.datadog.android.api.context.LocaleInfo + constructor(List, String, String) +data class com.datadog.android.api.context.NetworkInfo + constructor(Connectivity = Connectivity.NETWORK_NOT_CONNECTED, kotlin.String? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.String? = null) + enum Connectivity + constructor(kotlin.String) + - NETWORK_NOT_CONNECTED + - NETWORK_ETHERNET + - NETWORK_WIFI + - NETWORK_WIMAX + - NETWORK_BLUETOOTH + - NETWORK_2G + - NETWORK_3G + - NETWORK_4G + - NETWORK_5G + - NETWORK_MOBILE_OTHER + - NETWORK_CELLULAR + - NETWORK_OTHER +data class com.datadog.android.api.context.ProcessInfo + constructor(Boolean) +data class com.datadog.android.api.context.TimeInfo + constructor(Long, Long, Long, Long) +data class com.datadog.android.api.context.UserInfo + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, Map = emptyMap()) +interface com.datadog.android.api.feature.Feature + val name: String + fun onInitialize(android.content.Context) + fun onStop() + companion object + const val LOGS_FEATURE_NAME: String + const val RUM_FEATURE_NAME: String + const val TRACING_FEATURE_NAME: String + const val SESSION_REPLAY_FEATURE_NAME: String + const val SESSION_REPLAY_RESOURCES_FEATURE_NAME: String + const val NDK_CRASH_REPORTS_FEATURE_NAME: String +interface com.datadog.android.api.feature.FeatureContextUpdateReceiver + fun onContextUpdate(String, Map) +interface com.datadog.android.api.feature.FeatureEventReceiver + fun onReceive(Any) +interface com.datadog.android.api.feature.FeatureScope + val dataStore: com.datadog.android.api.storage.datastore.DataStoreHandler + fun withWriteContext(Set = emptySet(), (com.datadog.android.api.context.DatadogContext) -> Unit) + fun withContext(Set = emptySet(), (com.datadog.android.api.context.DatadogContext) -> Unit) + fun getWriteContextSync(Set = emptySet()): Pair? + fun sendEvent(Any) + fun unwrap(): T +typealias EventWriteScope = ((com.datadog.android.api.storage.EventBatchWriter) -> Unit) -> Unit +fun com.datadog.android.api.InternalLogger.measureMethodCallPerf(Class<*>, String, Float = 100f, () -> R): R +fun FeatureScope.getContextFuture(Set = emptySet()): java.util.concurrent.Future? +interface com.datadog.android.api.feature.FeatureSdkCore : com.datadog.android.api.SdkCore + val internalLogger: com.datadog.android.api.InternalLogger + fun registerFeature(Feature) + fun getFeature(String): FeatureScope? + fun updateFeatureContext(String, Boolean = true, (MutableMap) -> Unit) + fun getFeatureContext(String, Boolean = true): Map + fun setEventReceiver(String, FeatureEventReceiver) + fun removeEventReceiver(String) + fun setContextUpdateReceiver(FeatureContextUpdateReceiver) + fun removeContextUpdateReceiver(FeatureContextUpdateReceiver) + fun createSingleThreadExecutorService(String): java.util.concurrent.ExecutorService + fun createScheduledExecutorService(String): java.util.concurrent.ScheduledExecutorService + fun setAnonymousId(java.util.UUID?) +interface com.datadog.android.api.feature.StorageBackedFeature : Feature + val requestFactory: com.datadog.android.api.net.RequestFactory + val storageConfiguration: com.datadog.android.api.storage.FeatureStorageConfiguration +data class com.datadog.android.api.net.Request + constructor(String, String, String, Map, ByteArray, String? = null) +data class com.datadog.android.api.net.RequestExecutionContext + constructor(Int = 0, Int? = null) +interface com.datadog.android.api.net.RequestFactory + fun create(com.datadog.android.api.context.DatadogContext, RequestExecutionContext, List, ByteArray?): Request? + companion object + const val CONTENT_TYPE_JSON: String + const val CONTENT_TYPE_TEXT_UTF8: String + const val HEADER_API_KEY: String + const val HEADER_EVP_ORIGIN: String + const val HEADER_EVP_ORIGIN_VERSION: String + const val HEADER_REQUEST_ID: String + const val QUERY_PARAM_SOURCE: String + const val QUERY_PARAM_TAGS: String + const val DD_IDEMPOTENCY_KEY: String +interface com.datadog.android.api.storage.DataWriter + fun write(EventBatchWriter, T, EventType): Boolean +interface com.datadog.android.api.storage.EventBatchWriter + fun currentMetadata(): ByteArray? + fun write(RawBatchEvent, ByteArray?, EventType): Boolean +enum com.datadog.android.api.storage.EventType + - DEFAULT + - CRASH + - TELEMETRY +data class com.datadog.android.api.storage.FeatureStorageConfiguration + constructor(Long, Int, Long, Long) + companion object + val DEFAULT: FeatureStorageConfiguration +data class com.datadog.android.api.storage.RawBatchEvent + constructor(ByteArray, ByteArray = EMPTY_BYTE_ARRAY) + override fun equals(Any?): Boolean + override fun hashCode(): Int +interface com.datadog.android.api.storage.datastore.DataStoreHandler + fun setValue(String, T, Int = 0, DataStoreWriteCallback? = null, com.datadog.android.core.persistence.Serializer) + fun value(String, Int? = null, DataStoreReadCallback, com.datadog.android.core.internal.persistence.Deserializer) + fun removeValue(String, DataStoreWriteCallback? = null) + fun clearAllData() + companion object + const val CURRENT_DATASTORE_VERSION: Int +interface com.datadog.android.api.storage.datastore.DataStoreReadCallback + fun onSuccess(com.datadog.android.core.persistence.datastore.DataStoreContent?) + fun onFailure() +interface com.datadog.android.api.storage.datastore.DataStoreWriteCallback + fun onSuccess() + fun onFailure() +interface com.datadog.android.core.InternalSdkCore : com.datadog.android.api.feature.FeatureSdkCore + val networkInfo: com.datadog.android.api.context.NetworkInfo + val trackingConsent: com.datadog.android.privacy.TrackingConsent + val rootStorageDir: java.io.File? + val isDeveloperModeEnabled: Boolean + val firstPartyHostResolver: com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver + val lastViewEvent: com.google.gson.JsonObject? + val lastFatalAnrSent: Long? + val appStartTimeNs: Long + fun writeLastViewEvent(ByteArray) + fun deleteLastViewEvent() + fun writeLastFatalAnrSent(Long) + fun getPersistenceExecutorService(): java.util.concurrent.ExecutorService + fun getAllFeatures(): List + fun getDatadogContext(Set = emptySet()): com.datadog.android.api.context.DatadogContext? +class com.datadog.android.core.SdkReference + constructor(String? = null, (com.datadog.android.api.SdkCore) -> Unit = {}) + fun get(): com.datadog.android.api.SdkCore? +class com.datadog.android.core.UploadWorker : androidx.work.Worker + constructor(android.content.Context, androidx.work.WorkerParameters) + override fun doWork(): Result + companion object +enum com.datadog.android.core.configuration.BackPressureMitigation + - DROP_OLDEST + - IGNORE_NEWEST +data class com.datadog.android.core.configuration.BackPressureStrategy + constructor(Int, () -> Unit, (Any) -> Unit, BackPressureMitigation) +enum com.datadog.android.core.configuration.BatchProcessingLevel + constructor(Int) + - LOW + - MEDIUM + - HIGH +enum com.datadog.android.core.configuration.BatchSize + constructor(Long) + - SMALL + - MEDIUM + - LARGE +data class com.datadog.android.core.configuration.Configuration + class Builder + constructor(String, String, String = NO_VARIANT, String? = null) + fun build(): Configuration + fun setUseDeveloperModeWhenDebuggable(Boolean): Builder + fun setFirstPartyHosts(List): Builder + fun setFirstPartyHostsWithHeaderType(Map>): Builder + fun useSite(com.datadog.android.DatadogSite): Builder + fun setBatchSize(BatchSize): Builder + fun setUploadFrequency(UploadFrequency): Builder + fun setBatchProcessingLevel(BatchProcessingLevel): Builder + fun setAdditionalConfiguration(Map): Builder + fun setProxy(java.net.Proxy, okhttp3.Authenticator?): Builder + fun setEncryption(com.datadog.android.security.Encryption): Builder + fun setPersistenceStrategyFactory(com.datadog.android.core.persistence.PersistenceStrategy.Factory?): Builder + fun setCrashReportsEnabled(Boolean): Builder + fun setBackpressureStrategy(BackPressureStrategy): Builder + fun setUploadSchedulerStrategy(UploadSchedulerStrategy?): Builder + companion object +class com.datadog.android.core.configuration.HostsSanitizer + fun sanitizeHosts(List, String): List +enum com.datadog.android.core.configuration.UploadFrequency + constructor(Long) + - FREQUENT + - AVERAGE + - RARE +interface com.datadog.android.core.configuration.UploadSchedulerStrategy + fun getMsDelayUntilNextUpload(String, Int, Int?, Throwable?): Long +interface com.datadog.android.core.constraints.DataConstraints + fun validateAttributes(Map, String? = null, String? = null, Set = emptySet()): MutableMap + fun validateTags(List): List + fun validateTimings(Map): MutableMap +class com.datadog.android.core.constraints.DatadogDataConstraints : DataConstraints + constructor(com.datadog.android.api.InternalLogger) + override fun validateTags(List): List + override fun validateAttributes(Map, String?, String?, Set): MutableMap + override fun validateTimings(Map): MutableMap +sealed class com.datadog.android.core.feature.event.JvmCrash + abstract val throwable: Throwable + abstract val message: String + abstract val threads: List + data class Rum : JvmCrash + constructor(Throwable, String, List) +data class com.datadog.android.core.feature.event.ThreadDump + constructor(String, String, String, Boolean) +class com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver : FirstPartyHostHeaderTypeResolver + constructor(Map>) + override fun isFirstPartyUrl(okhttp3.HttpUrl): Boolean + override fun isFirstPartyUrl(String): Boolean + override fun headerTypesForUrl(String): Set + override fun headerTypesForUrl(okhttp3.HttpUrl): Set + override fun getAllHeaderTypes(): Set + override fun isEmpty(): Boolean +interface com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver + fun isFirstPartyUrl(okhttp3.HttpUrl): Boolean + fun isFirstPartyUrl(String): Boolean + fun headerTypesForUrl(String): Set + fun headerTypesForUrl(okhttp3.HttpUrl): Set + fun getAllHeaderTypes(): Set + fun isEmpty(): Boolean +interface com.datadog.android.core.internal.persistence.Deserializer + fun deserialize(P): R? +fun java.io.File.canReadSafe(com.datadog.android.api.InternalLogger): Boolean +fun java.io.File.existsSafe(com.datadog.android.api.InternalLogger): Boolean +fun java.io.File.listFilesSafe(com.datadog.android.api.InternalLogger, java.io.FilenameFilter): Array? +fun java.io.File.readTextSafe(java.nio.charset.Charset = Charsets.UTF_8, com.datadog.android.api.InternalLogger): String? +fun java.io.File.readLinesSafe(java.nio.charset.Charset = Charsets.UTF_8, com.datadog.android.api.InternalLogger): List? +interface com.datadog.android.core.internal.system.BuildSdkVersionProvider + val version: Int + companion object + val DEFAULT: BuildSdkVersionProvider +fun Collection.join(ByteArray, ByteArray = ByteArray(0), ByteArray = ByteArray(0), com.datadog.android.api.InternalLogger): ByteArray +fun java.util.concurrent.Executor.executeSafe(String, com.datadog.android.api.InternalLogger, Runnable) +fun java.util.concurrent.ScheduledExecutorService.scheduleSafe(String, Long, java.util.concurrent.TimeUnit, com.datadog.android.api.InternalLogger, Runnable): java.util.concurrent.ScheduledFuture<*>? +fun java.util.concurrent.ExecutorService.submitSafe(String, com.datadog.android.api.InternalLogger, Runnable): java.util.concurrent.Future<*>? +fun java.util.concurrent.ExecutorService.submitSafe(String, com.datadog.android.api.InternalLogger, java.util.concurrent.Callable): java.util.concurrent.Future? +fun java.util.concurrent.Future?.getSafe(String, com.datadog.android.api.InternalLogger): T? +object com.datadog.android.core.internal.utils.JsonSerializer + fun toJsonElement(Any?): com.google.gson.JsonElement + fun Map.safeMapValuesToJson(com.datadog.android.api.InternalLogger): Map +enum com.datadog.android.core.metrics.MethodCallSamplingRate + constructor(Float) + - ALL + - HIGH + - MEDIUM + - LOW + - REDUCED + - RARE +interface com.datadog.android.core.metrics.PerformanceMetric + fun stopAndSend(Boolean) + companion object + const val METRIC_TYPE: String +enum com.datadog.android.core.metrics.TelemetryMetricType + - MethodCalled +interface com.datadog.android.core.persistence.PersistenceStrategy + interface Factory + fun create(String, Int, Long): PersistenceStrategy + data class Batch + constructor(String, ByteArray? = null, List = mutableListOf()) + fun currentMetadata(): ByteArray? + fun write(com.datadog.android.api.storage.RawBatchEvent, ByteArray?, com.datadog.android.api.storage.EventType): Boolean + fun lockAndReadNext(): Batch? + fun unlockAndKeep(String) + fun unlockAndDelete(String) + fun dropAll() + fun migrateData(PersistenceStrategy) +interface com.datadog.android.core.persistence.Serializer + fun serialize(T): String? + companion object +fun Serializer.serializeToByteArray(T, com.datadog.android.api.InternalLogger): ByteArray? +data class com.datadog.android.core.persistence.datastore.DataStoreContent + constructor(Int, T?) +open class com.datadog.android.core.sampling.DeterministicSampler : Sampler + constructor((T) -> ULong, () -> Float) + constructor((T) -> ULong, Float) + constructor((T) -> ULong, Double) + override fun sample(T): Boolean + override fun getSampleRate(): Float + companion object + const val SAMPLE_ALL_RATE: Float + const val SAMPLER_HASHER: ULong + const val MAX_ID: ULong +open class com.datadog.android.core.sampling.RateBasedSampler : Sampler + constructor(() -> Float) + constructor(Float) + constructor(Double) + override fun sample(T): Boolean + override fun getSampleRate(): Float +interface com.datadog.android.core.sampling.Sampler + fun sample(T): Boolean + fun getSampleRate(): Float? +interface com.datadog.android.core.thread.FlushableExecutorService : java.util.concurrent.ExecutorService + fun drainTo(MutableCollection) + interface Factory + fun create(com.datadog.android.api.InternalLogger, String, com.datadog.android.core.configuration.BackPressureStrategy): FlushableExecutorService +interface com.datadog.android.event.EventMapper + fun map(T): T? +class com.datadog.android.event.MapperSerializer : com.datadog.android.core.persistence.Serializer + constructor(EventMapper, com.datadog.android.core.persistence.Serializer) + override fun serialize(T): String? +class com.datadog.android.event.NoOpEventMapper : EventMapper + override fun map(T): T + override fun equals(Any?): Boolean + override fun hashCode(): Int +annotation com.datadog.android.lint.InternalApi +object com.datadog.android.log.LogAttributes + const val APPLICATION_PACKAGE: String + const val APPLICATION_VERSION: String + const val ENV: String + const val DATE: String + const val DB_INSTANCE: String + const val DB_OPERATION: String + const val DB_STATEMENT: String + const val DB_USER: String + const val DD_SPAN_ID: String + const val DD_TRACE_ID: String + const val DURATION: String + const val ERROR_KIND: String + const val ERROR_MESSAGE: String + const val ERROR_STACK: String + const val ERROR_SOURCE_TYPE: String + const val HOST: String + const val HTTP_METHOD: String + const val HTTP_REFERRER: String + const val HTTP_REQUEST_ID: String + const val HTTP_STATUS_CODE: String + const val HTTP_URL: String + const val HTTP_USERAGENT: String + const val HTTP_VERSION: String + const val LOGGER_METHOD_NAME: String + const val LOGGER_NAME: String + const val LOGGER_THREAD_NAME: String + const val LOGGER_VERSION: String + const val MESSAGE: String + const val NETWORK_CARRIER_ID: String + const val NETWORK_CARRIER_NAME: String + const val NETWORK_CLIENT_IP: String + const val NETWORK_CLIENT_PORT: String + const val NETWORK_CONNECTIVITY: String + const val NETWORK_DOWN_KBPS: String + const val NETWORK_SIGNAL_STRENGTH: String + const val NETWORK_UP_KBPS: String + const val RUM_APPLICATION_ID: String + const val RUM_SESSION_ID: String + const val RUM_VIEW_ID: String + const val RUM_ACTION_ID: String + const val SERVICE_NAME: String + const val SOURCE: String + const val STATUS: String + const val USR_ATTRIBUTES_GROUP: String + const val USR_EMAIL: String + const val USR_ID: String + const val USR_NAME: String + const val ACCOUNT_ATTRIBUTES_GROUP: String + const val ACCOUNT_ID: String + const val ACCOUNT_NAME: String + const val VARIANT: String + const val SOURCE_TYPE: String + const val ERROR_FINGERPRINT: String +enum com.datadog.android.privacy.TrackingConsent + - GRANTED + - NOT_GRANTED + - PENDING +interface com.datadog.android.privacy.TrackingConsentProviderCallback + fun onConsentUpdated(TrackingConsent, TrackingConsent) +interface com.datadog.android.security.Encryption + fun encrypt(ByteArray): ByteArray + fun decrypt(ByteArray): ByteArray +enum com.datadog.android.trace.TracingHeaderType + constructor(String) + - DATADOG + - B3 + - B3MULTI + - TRACECONTEXT diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api new file mode 100644 index 0000000000..a72660bc66 --- /dev/null +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -0,0 +1,1056 @@ +public final class com/datadog/android/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public static final field LOGCAT_ENABLED Ljava/lang/Boolean; + public static final field SDK_VERSION_CODE I + public static final field SDK_VERSION_NAME Ljava/lang/String; + public fun ()V +} + +public final class com/datadog/android/Datadog { + public static final field INSTANCE Lcom/datadog/android/Datadog; + public final fun _internalProxy (Ljava/lang/String;)Lcom/datadog/android/_InternalProxy; + public static synthetic fun _internalProxy$default (Lcom/datadog/android/Datadog;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/_InternalProxy; + public static final fun addAccountExtraInfo (Ljava/util/Map;)V + public static final fun addAccountExtraInfo (Ljava/util/Map;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun addAccountExtraInfo$default (Ljava/util/Map;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun addUserProperties (Ljava/util/Map;)V + public static final fun addUserProperties (Ljava/util/Map;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun addUserProperties$default (Ljava/util/Map;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun clearAccountInfo ()V + public static final fun clearAccountInfo (Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun clearAccountInfo$default (Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun clearAllData ()V + public static final fun clearAllData (Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun clearAllData$default (Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun clearUserInfo ()V + public static final fun clearUserInfo (Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun clearUserInfo$default (Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun getInstance ()Lcom/datadog/android/api/SdkCore; + public static final fun getInstance (Ljava/lang/String;)Lcom/datadog/android/api/SdkCore; + public static synthetic fun getInstance$default (Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/api/SdkCore; + public static final fun getVerbosity ()I + public static final fun initialize (Landroid/content/Context;Lcom/datadog/android/core/configuration/Configuration;Lcom/datadog/android/privacy/TrackingConsent;)Lcom/datadog/android/api/SdkCore; + public static final fun initialize (Ljava/lang/String;Landroid/content/Context;Lcom/datadog/android/core/configuration/Configuration;Lcom/datadog/android/privacy/TrackingConsent;)Lcom/datadog/android/api/SdkCore; + public static final fun isInitialized ()Z + public static final fun isInitialized (Ljava/lang/String;)Z + public static synthetic fun isInitialized$default (Ljava/lang/String;ILjava/lang/Object;)Z + public static final fun setAccountInfo (Ljava/lang/String;)V + public static final fun setAccountInfo (Ljava/lang/String;Ljava/lang/String;)V + public static final fun setAccountInfo (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public static final fun setAccountInfo (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun setAccountInfo$default (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun setTrackingConsent (Lcom/datadog/android/privacy/TrackingConsent;)V + public static final fun setTrackingConsent (Lcom/datadog/android/privacy/TrackingConsent;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun setTrackingConsent$default (Lcom/datadog/android/privacy/TrackingConsent;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun setUserInfo (Ljava/lang/String;)V + public static final fun setUserInfo (Ljava/lang/String;Ljava/lang/String;)V + public static final fun setUserInfo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public static final fun setUserInfo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public static final fun setUserInfo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun setUserInfo$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun setVerbosity (I)V + public static final fun stopInstance ()V + public static final fun stopInstance (Ljava/lang/String;)V + public static synthetic fun stopInstance$default (Ljava/lang/String;ILjava/lang/Object;)V +} + +public final class com/datadog/android/DatadogSite : java/lang/Enum { + public static final field AP1 Lcom/datadog/android/DatadogSite; + public static final field AP2 Lcom/datadog/android/DatadogSite; + public static final field EU1 Lcom/datadog/android/DatadogSite; + public static final field STAGING Lcom/datadog/android/DatadogSite; + public static final field US1 Lcom/datadog/android/DatadogSite; + public static final field US1_FED Lcom/datadog/android/DatadogSite; + public static final field US3 Lcom/datadog/android/DatadogSite; + public static final field US5 Lcom/datadog/android/DatadogSite; + public final fun getIntakeEndpoint ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/DatadogSite; + public static fun values ()[Lcom/datadog/android/DatadogSite; +} + +public final class com/datadog/android/_InternalProxy { + public static final field Companion Lcom/datadog/android/_InternalProxy$Companion; + public final fun get_telemetry ()Lcom/datadog/android/_InternalProxy$_TelemetryProxy; + public final fun setCustomAppVersion (Ljava/lang/String;)V +} + +public final class com/datadog/android/_InternalProxy$Companion { + public final fun allowClearTextHttp (Lcom/datadog/android/core/configuration/Configuration$Builder;)Lcom/datadog/android/core/configuration/Configuration$Builder; +} + +public final class com/datadog/android/_InternalProxy$_TelemetryProxy { + public final fun debug (Ljava/lang/String;)V + public final fun error (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V + public static synthetic fun error$default (Lcom/datadog/android/_InternalProxy$_TelemetryProxy;Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V +} + +public abstract interface class com/datadog/android/api/InternalLogger { + public static final field Companion Lcom/datadog/android/api/InternalLogger$Companion; + public abstract fun log (Lcom/datadog/android/api/InternalLogger$Level;Lcom/datadog/android/api/InternalLogger$Target;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;)V + public abstract fun log (Lcom/datadog/android/api/InternalLogger$Level;Ljava/util/List;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;)V + public abstract fun logApiUsage (FLkotlin/jvm/functions/Function0;)V + public abstract fun logMetric (Lkotlin/jvm/functions/Function0;Ljava/util/Map;FLjava/lang/Float;)V + public abstract fun startPerformanceMeasure (Ljava/lang/String;Lcom/datadog/android/core/metrics/TelemetryMetricType;FLjava/lang/String;)Lcom/datadog/android/core/metrics/PerformanceMetric; +} + +public final class com/datadog/android/api/InternalLogger$Companion { + public final fun getUNBOUND ()Lcom/datadog/android/api/InternalLogger; +} + +public final class com/datadog/android/api/InternalLogger$DefaultImpls { + public static synthetic fun log$default (Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/api/InternalLogger$Level;Lcom/datadog/android/api/InternalLogger$Target;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;ILjava/lang/Object;)V + public static synthetic fun log$default (Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/api/InternalLogger$Level;Ljava/util/List;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;ILjava/lang/Object;)V + public static synthetic fun logApiUsage$default (Lcom/datadog/android/api/InternalLogger;FLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static synthetic fun logMetric$default (Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/functions/Function0;Ljava/util/Map;FLjava/lang/Float;ILjava/lang/Object;)V +} + +public final class com/datadog/android/api/InternalLogger$Level : java/lang/Enum { + public static final field DEBUG Lcom/datadog/android/api/InternalLogger$Level; + public static final field ERROR Lcom/datadog/android/api/InternalLogger$Level; + public static final field INFO Lcom/datadog/android/api/InternalLogger$Level; + public static final field VERBOSE Lcom/datadog/android/api/InternalLogger$Level; + public static final field WARN Lcom/datadog/android/api/InternalLogger$Level; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/api/InternalLogger$Level; + public static fun values ()[Lcom/datadog/android/api/InternalLogger$Level; +} + +public final class com/datadog/android/api/InternalLogger$Target : java/lang/Enum { + public static final field MAINTAINER Lcom/datadog/android/api/InternalLogger$Target; + public static final field TELEMETRY Lcom/datadog/android/api/InternalLogger$Target; + public static final field USER Lcom/datadog/android/api/InternalLogger$Target; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/api/InternalLogger$Target; + public static fun values ()[Lcom/datadog/android/api/InternalLogger$Target; +} + +public abstract interface class com/datadog/android/api/SdkCore { + public abstract fun addAccountExtraInfo (Ljava/util/Map;)V + public abstract fun addUserProperties (Ljava/util/Map;)V + public abstract fun clearAccountInfo ()V + public abstract fun clearAllData ()V + public abstract fun clearUserInfo ()V + public abstract fun getName ()Ljava/lang/String; + public abstract fun getService ()Ljava/lang/String; + public abstract fun getTime ()Lcom/datadog/android/api/context/TimeInfo; + public abstract fun isCoreActive ()Z + public abstract fun setAccountInfo (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public abstract fun setTrackingConsent (Lcom/datadog/android/privacy/TrackingConsent;)V + public abstract fun setUserInfo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V +} + +public final class com/datadog/android/api/SdkCore$DefaultImpls { + public static synthetic fun setAccountInfo$default (Lcom/datadog/android/api/SdkCore;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V + public static synthetic fun setUserInfo$default (Lcom/datadog/android/api/SdkCore;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V +} + +public final class com/datadog/android/api/context/AccountInfo { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/api/context/AccountInfo; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/AccountInfo;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/api/context/AccountInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getExtraInfo ()Ljava/util/Map; + public final fun getId ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/context/DatadogContext { + public fun (Lcom/datadog/android/DatadogSite;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/TimeInfo;Lcom/datadog/android/api/context/ProcessInfo;Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/DeviceInfo;Lcom/datadog/android/api/context/UserInfo;Lcom/datadog/android/api/context/AccountInfo;Lcom/datadog/android/privacy/TrackingConsent;Ljava/lang/String;Ljava/util/Map;)V + public final fun component1 ()Lcom/datadog/android/DatadogSite; + public final fun component10 ()Lcom/datadog/android/api/context/ProcessInfo; + public final fun component11 ()Lcom/datadog/android/api/context/NetworkInfo; + public final fun component12 ()Lcom/datadog/android/api/context/DeviceInfo; + public final fun component13 ()Lcom/datadog/android/api/context/UserInfo; + public final fun component14 ()Lcom/datadog/android/api/context/AccountInfo; + public final fun component15 ()Lcom/datadog/android/privacy/TrackingConsent; + public final fun component16 ()Ljava/lang/String; + public final fun component17 ()Ljava/util/Map; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Lcom/datadog/android/api/context/TimeInfo; + public final fun copy (Lcom/datadog/android/DatadogSite;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/TimeInfo;Lcom/datadog/android/api/context/ProcessInfo;Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/DeviceInfo;Lcom/datadog/android/api/context/UserInfo;Lcom/datadog/android/api/context/AccountInfo;Lcom/datadog/android/privacy/TrackingConsent;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/api/context/DatadogContext; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/DatadogSite;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/TimeInfo;Lcom/datadog/android/api/context/ProcessInfo;Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/DeviceInfo;Lcom/datadog/android/api/context/UserInfo;Lcom/datadog/android/api/context/AccountInfo;Lcom/datadog/android/privacy/TrackingConsent;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/api/context/DatadogContext; + public fun equals (Ljava/lang/Object;)Z + public final fun getAccountInfo ()Lcom/datadog/android/api/context/AccountInfo; + public final fun getAppBuildId ()Ljava/lang/String; + public final fun getClientToken ()Ljava/lang/String; + public final fun getDeviceInfo ()Lcom/datadog/android/api/context/DeviceInfo; + public final fun getEnv ()Ljava/lang/String; + public final fun getFeaturesContext ()Ljava/util/Map; + public final fun getNetworkInfo ()Lcom/datadog/android/api/context/NetworkInfo; + public final fun getProcessInfo ()Lcom/datadog/android/api/context/ProcessInfo; + public final fun getSdkVersion ()Ljava/lang/String; + public final fun getService ()Ljava/lang/String; + public final fun getSite ()Lcom/datadog/android/DatadogSite; + public final fun getSource ()Ljava/lang/String; + public final fun getTime ()Lcom/datadog/android/api/context/TimeInfo; + public final fun getTrackingConsent ()Lcom/datadog/android/privacy/TrackingConsent; + public final fun getUserInfo ()Lcom/datadog/android/api/context/UserInfo; + public final fun getVariant ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/context/DeviceInfo { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/DeviceType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/context/LocaleInfo;)V + public final fun component1 ()Ljava/lang/String; + public final fun component10 ()Ljava/lang/Integer; + public final fun component11 ()Lcom/datadog/android/api/context/LocaleInfo; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lcom/datadog/android/api/context/DeviceType; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/DeviceType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/context/LocaleInfo;)Lcom/datadog/android/api/context/DeviceInfo; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/DeviceInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/DeviceType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/context/LocaleInfo;ILjava/lang/Object;)Lcom/datadog/android/api/context/DeviceInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getArchitecture ()Ljava/lang/String; + public final fun getDeviceBrand ()Ljava/lang/String; + public final fun getDeviceBuildId ()Ljava/lang/String; + public final fun getDeviceModel ()Ljava/lang/String; + public final fun getDeviceName ()Ljava/lang/String; + public final fun getDeviceType ()Lcom/datadog/android/api/context/DeviceType; + public final fun getLocaleInfo ()Lcom/datadog/android/api/context/LocaleInfo; + public final fun getNumberOfDisplays ()Ljava/lang/Integer; + public final fun getOsMajorVersion ()Ljava/lang/String; + public final fun getOsName ()Ljava/lang/String; + public final fun getOsVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/context/DeviceType : java/lang/Enum { + public static final field BOT Lcom/datadog/android/api/context/DeviceType; + public static final field DESKTOP Lcom/datadog/android/api/context/DeviceType; + public static final field GAMING_CONSOLE Lcom/datadog/android/api/context/DeviceType; + public static final field MOBILE Lcom/datadog/android/api/context/DeviceType; + public static final field OTHER Lcom/datadog/android/api/context/DeviceType; + public static final field TABLET Lcom/datadog/android/api/context/DeviceType; + public static final field TV Lcom/datadog/android/api/context/DeviceType; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/api/context/DeviceType; + public static fun values ()[Lcom/datadog/android/api/context/DeviceType; +} + +public final class com/datadog/android/api/context/LocaleInfo { + public fun (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/api/context/LocaleInfo; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/LocaleInfo;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/api/context/LocaleInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getCurrentLocale ()Ljava/lang/String; + public final fun getLocales ()Ljava/util/List; + public final fun getTimeZone ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/context/NetworkInfo { + public fun ()V + public fun (Lcom/datadog/android/api/context/NetworkInfo$Connectivity;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/String;)V + public synthetic fun (Lcom/datadog/android/api/context/NetworkInfo$Connectivity;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Long; + public final fun component4 ()Ljava/lang/Long; + public final fun component5 ()Ljava/lang/Long; + public final fun component6 ()Ljava/lang/Long; + public final fun component7 ()Ljava/lang/String; + public final fun copy (Lcom/datadog/android/api/context/NetworkInfo$Connectivity;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/String;)Lcom/datadog/android/api/context/NetworkInfo; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/NetworkInfo$Connectivity;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/api/context/NetworkInfo; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/api/context/NetworkInfo; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/api/context/NetworkInfo; + public final fun getCarrierId ()Ljava/lang/Long; + public final fun getCarrierName ()Ljava/lang/String; + public final fun getCellularTechnology ()Ljava/lang/String; + public final fun getConnectivity ()Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public final fun getDownKbps ()Ljava/lang/Long; + public final fun getStrength ()Ljava/lang/Long; + public final fun getUpKbps ()Ljava/lang/Long; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/context/NetworkInfo$Connectivity : java/lang/Enum { + public static final field NETWORK_2G Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_3G Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_4G Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_5G Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_BLUETOOTH Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_CELLULAR Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_ETHERNET Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_MOBILE_OTHER Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_NOT_CONNECTED Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_OTHER Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_WIFI Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final field NETWORK_WIMAX Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/api/context/NetworkInfo$Connectivity; + public static fun values ()[Lcom/datadog/android/api/context/NetworkInfo$Connectivity; +} + +public final class com/datadog/android/api/context/ProcessInfo { + public fun (Z)V + public final fun component1 ()Z + public final fun copy (Z)Lcom/datadog/android/api/context/ProcessInfo; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/ProcessInfo;ZILjava/lang/Object;)Lcom/datadog/android/api/context/ProcessInfo; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public final fun isMainProcess ()Z + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/context/TimeInfo { + public fun (JJJJ)V + public final fun component1 ()J + public final fun component2 ()J + public final fun component3 ()J + public final fun component4 ()J + public final fun copy (JJJJ)Lcom/datadog/android/api/context/TimeInfo; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/TimeInfo;JJJJILjava/lang/Object;)Lcom/datadog/android/api/context/TimeInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getDeviceTimeNs ()J + public final fun getServerTimeNs ()J + public final fun getServerTimeOffsetMs ()J + public final fun getServerTimeOffsetNs ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/context/UserInfo { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/api/context/UserInfo; + public static synthetic fun copy$default (Lcom/datadog/android/api/context/UserInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/api/context/UserInfo; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/api/context/UserInfo; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/api/context/UserInfo; + public final fun getAdditionalProperties ()Ljava/util/Map; + public final fun getAnonymousId ()Ljava/lang/String; + public final fun getEmail ()Ljava/lang/String; + public final fun getId ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/datadog/android/api/feature/Feature { + public static final field Companion Lcom/datadog/android/api/feature/Feature$Companion; + public static final field LOGS_FEATURE_NAME Ljava/lang/String; + public static final field NDK_CRASH_REPORTS_FEATURE_NAME Ljava/lang/String; + public static final field RUM_FEATURE_NAME Ljava/lang/String; + public static final field SESSION_REPLAY_FEATURE_NAME Ljava/lang/String; + public static final field SESSION_REPLAY_RESOURCES_FEATURE_NAME Ljava/lang/String; + public static final field TRACING_FEATURE_NAME Ljava/lang/String; + public abstract fun getName ()Ljava/lang/String; + public abstract fun onInitialize (Landroid/content/Context;)V + public abstract fun onStop ()V +} + +public final class com/datadog/android/api/feature/Feature$Companion { + public static final field LOGS_FEATURE_NAME Ljava/lang/String; + public static final field NDK_CRASH_REPORTS_FEATURE_NAME Ljava/lang/String; + public static final field RUM_FEATURE_NAME Ljava/lang/String; + public static final field SESSION_REPLAY_FEATURE_NAME Ljava/lang/String; + public static final field SESSION_REPLAY_RESOURCES_FEATURE_NAME Ljava/lang/String; + public static final field TRACING_FEATURE_NAME Ljava/lang/String; +} + +public abstract interface class com/datadog/android/api/feature/FeatureContextUpdateReceiver { + public abstract fun onContextUpdate (Ljava/lang/String;Ljava/util/Map;)V +} + +public abstract interface class com/datadog/android/api/feature/FeatureEventReceiver { + public abstract fun onReceive (Ljava/lang/Object;)V +} + +public abstract interface class com/datadog/android/api/feature/FeatureScope { + public abstract fun getDataStore ()Lcom/datadog/android/api/storage/datastore/DataStoreHandler; + public abstract fun getWriteContextSync (Ljava/util/Set;)Lkotlin/Pair; + public abstract fun sendEvent (Ljava/lang/Object;)V + public abstract fun unwrap ()Lcom/datadog/android/api/feature/Feature; + public abstract fun withContext (Ljava/util/Set;Lkotlin/jvm/functions/Function1;)V + public abstract fun withWriteContext (Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V +} + +public final class com/datadog/android/api/feature/FeatureScope$DefaultImpls { + public static synthetic fun getWriteContextSync$default (Lcom/datadog/android/api/feature/FeatureScope;Ljava/util/Set;ILjava/lang/Object;)Lkotlin/Pair; + public static synthetic fun withContext$default (Lcom/datadog/android/api/feature/FeatureScope;Ljava/util/Set;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun withWriteContext$default (Lcom/datadog/android/api/feature/FeatureScope;Ljava/util/Set;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V +} + +public final class com/datadog/android/api/feature/FeatureScopeExtKt { + public static final fun getContextFuture (Lcom/datadog/android/api/feature/FeatureScope;Ljava/util/Set;)Ljava/util/concurrent/Future; + public static synthetic fun getContextFuture$default (Lcom/datadog/android/api/feature/FeatureScope;Ljava/util/Set;ILjava/lang/Object;)Ljava/util/concurrent/Future; + public static final fun measureMethodCallPerf (Lcom/datadog/android/api/InternalLogger;Ljava/lang/Class;Ljava/lang/String;FLkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public static synthetic fun measureMethodCallPerf$default (Lcom/datadog/android/api/InternalLogger;Ljava/lang/Class;Ljava/lang/String;FLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Ljava/lang/Object; +} + +public abstract interface class com/datadog/android/api/feature/FeatureSdkCore : com/datadog/android/api/SdkCore { + public abstract fun createScheduledExecutorService (Ljava/lang/String;)Ljava/util/concurrent/ScheduledExecutorService; + public abstract fun createSingleThreadExecutorService (Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; + public abstract fun getFeature (Ljava/lang/String;)Lcom/datadog/android/api/feature/FeatureScope; + public abstract fun getFeatureContext (Ljava/lang/String;Z)Ljava/util/Map; + public abstract fun getInternalLogger ()Lcom/datadog/android/api/InternalLogger; + public abstract fun registerFeature (Lcom/datadog/android/api/feature/Feature;)V + public abstract fun removeContextUpdateReceiver (Lcom/datadog/android/api/feature/FeatureContextUpdateReceiver;)V + public abstract fun removeEventReceiver (Ljava/lang/String;)V + public abstract fun setAnonymousId (Ljava/util/UUID;)V + public abstract fun setContextUpdateReceiver (Lcom/datadog/android/api/feature/FeatureContextUpdateReceiver;)V + public abstract fun setEventReceiver (Ljava/lang/String;Lcom/datadog/android/api/feature/FeatureEventReceiver;)V + public abstract fun updateFeatureContext (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V +} + +public final class com/datadog/android/api/feature/FeatureSdkCore$DefaultImpls { + public static synthetic fun getFeatureContext$default (Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;ZILjava/lang/Object;)Ljava/util/Map; + public static synthetic fun updateFeatureContext$default (Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public abstract interface class com/datadog/android/api/feature/StorageBackedFeature : com/datadog/android/api/feature/Feature { + public abstract fun getRequestFactory ()Lcom/datadog/android/api/net/RequestFactory; + public abstract fun getStorageConfiguration ()Lcom/datadog/android/api/storage/FeatureStorageConfiguration; +} + +public final class com/datadog/android/api/net/Request { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/util/Map; + public final fun component5 ()[B + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;)Lcom/datadog/android/api/net/Request; + public static synthetic fun copy$default (Lcom/datadog/android/api/net/Request;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/api/net/Request; + public fun equals (Ljava/lang/Object;)Z + public final fun getBody ()[B + public final fun getContentType ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getHeaders ()Ljava/util/Map; + public final fun getId ()Ljava/lang/String; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/net/RequestExecutionContext { + public fun ()V + public fun (ILjava/lang/Integer;)V + public synthetic fun (ILjava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component2 ()Ljava/lang/Integer; + public final fun copy (ILjava/lang/Integer;)Lcom/datadog/android/api/net/RequestExecutionContext; + public static synthetic fun copy$default (Lcom/datadog/android/api/net/RequestExecutionContext;ILjava/lang/Integer;ILjava/lang/Object;)Lcom/datadog/android/api/net/RequestExecutionContext; + public fun equals (Ljava/lang/Object;)Z + public final fun getAttemptNumber ()I + public final fun getPreviousResponseCode ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/datadog/android/api/net/RequestFactory { + public static final field CONTENT_TYPE_JSON Ljava/lang/String; + public static final field CONTENT_TYPE_TEXT_UTF8 Ljava/lang/String; + public static final field Companion Lcom/datadog/android/api/net/RequestFactory$Companion; + public static final field DD_IDEMPOTENCY_KEY Ljava/lang/String; + public static final field HEADER_API_KEY Ljava/lang/String; + public static final field HEADER_EVP_ORIGIN Ljava/lang/String; + public static final field HEADER_EVP_ORIGIN_VERSION Ljava/lang/String; + public static final field HEADER_REQUEST_ID Ljava/lang/String; + public static final field QUERY_PARAM_SOURCE Ljava/lang/String; + public static final field QUERY_PARAM_TAGS Ljava/lang/String; + public abstract fun create (Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/api/net/RequestExecutionContext;Ljava/util/List;[B)Lcom/datadog/android/api/net/Request; +} + +public final class com/datadog/android/api/net/RequestFactory$Companion { + public static final field CONTENT_TYPE_JSON Ljava/lang/String; + public static final field CONTENT_TYPE_TEXT_UTF8 Ljava/lang/String; + public static final field DD_IDEMPOTENCY_KEY Ljava/lang/String; + public static final field HEADER_API_KEY Ljava/lang/String; + public static final field HEADER_EVP_ORIGIN Ljava/lang/String; + public static final field HEADER_EVP_ORIGIN_VERSION Ljava/lang/String; + public static final field HEADER_REQUEST_ID Ljava/lang/String; + public static final field QUERY_PARAM_SOURCE Ljava/lang/String; + public static final field QUERY_PARAM_TAGS Ljava/lang/String; +} + +public abstract interface class com/datadog/android/api/storage/DataWriter { + public abstract fun write (Lcom/datadog/android/api/storage/EventBatchWriter;Ljava/lang/Object;Lcom/datadog/android/api/storage/EventType;)Z +} + +public abstract interface class com/datadog/android/api/storage/EventBatchWriter { + public abstract fun currentMetadata ()[B + public abstract fun write (Lcom/datadog/android/api/storage/RawBatchEvent;[BLcom/datadog/android/api/storage/EventType;)Z +} + +public final class com/datadog/android/api/storage/EventType : java/lang/Enum { + public static final field CRASH Lcom/datadog/android/api/storage/EventType; + public static final field DEFAULT Lcom/datadog/android/api/storage/EventType; + public static final field TELEMETRY Lcom/datadog/android/api/storage/EventType; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/api/storage/EventType; + public static fun values ()[Lcom/datadog/android/api/storage/EventType; +} + +public final class com/datadog/android/api/storage/FeatureStorageConfiguration { + public static final field Companion Lcom/datadog/android/api/storage/FeatureStorageConfiguration$Companion; + public fun (JIJJ)V + public final fun component1 ()J + public final fun component2 ()I + public final fun component3 ()J + public final fun component4 ()J + public final fun copy (JIJJ)Lcom/datadog/android/api/storage/FeatureStorageConfiguration; + public static synthetic fun copy$default (Lcom/datadog/android/api/storage/FeatureStorageConfiguration;JIJJILjava/lang/Object;)Lcom/datadog/android/api/storage/FeatureStorageConfiguration; + public fun equals (Ljava/lang/Object;)Z + public final fun getMaxBatchSize ()J + public final fun getMaxItemSize ()J + public final fun getMaxItemsPerBatch ()I + public final fun getOldBatchThreshold ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/api/storage/FeatureStorageConfiguration$Companion { + public final fun getDEFAULT ()Lcom/datadog/android/api/storage/FeatureStorageConfiguration; +} + +public final class com/datadog/android/api/storage/NoOpDataWriter : com/datadog/android/api/storage/DataWriter { + public fun ()V + public fun write (Lcom/datadog/android/api/storage/EventBatchWriter;Ljava/lang/Object;Lcom/datadog/android/api/storage/EventType;)Z +} + +public final class com/datadog/android/api/storage/RawBatchEvent { + public fun ([B[B)V + public synthetic fun ([B[BILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()[B + public final fun component2 ()[B + public final fun copy ([B[B)Lcom/datadog/android/api/storage/RawBatchEvent; + public static synthetic fun copy$default (Lcom/datadog/android/api/storage/RawBatchEvent;[B[BILjava/lang/Object;)Lcom/datadog/android/api/storage/RawBatchEvent; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()[B + public final fun getMetadata ()[B + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/datadog/android/api/storage/datastore/DataStoreHandler { + public static final field CURRENT_DATASTORE_VERSION I + public static final field Companion Lcom/datadog/android/api/storage/datastore/DataStoreHandler$Companion; + public abstract fun clearAllData ()V + public abstract fun removeValue (Ljava/lang/String;Lcom/datadog/android/api/storage/datastore/DataStoreWriteCallback;)V + public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/api/storage/datastore/DataStoreWriteCallback;Lcom/datadog/android/core/persistence/Serializer;)V + public abstract fun value (Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/storage/datastore/DataStoreReadCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;)V +} + +public final class com/datadog/android/api/storage/datastore/DataStoreHandler$Companion { + public static final field CURRENT_DATASTORE_VERSION I +} + +public final class com/datadog/android/api/storage/datastore/DataStoreHandler$DefaultImpls { + public static synthetic fun removeValue$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;Lcom/datadog/android/api/storage/datastore/DataStoreWriteCallback;ILjava/lang/Object;)V + public static synthetic fun setValue$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/api/storage/datastore/DataStoreWriteCallback;Lcom/datadog/android/core/persistence/Serializer;ILjava/lang/Object;)V + public static synthetic fun value$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/storage/datastore/DataStoreReadCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;ILjava/lang/Object;)V +} + +public abstract interface class com/datadog/android/api/storage/datastore/DataStoreReadCallback { + public abstract fun onFailure ()V + public abstract fun onSuccess (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;)V +} + +public abstract interface class com/datadog/android/api/storage/datastore/DataStoreWriteCallback { + public abstract fun onFailure ()V + public abstract fun onSuccess ()V +} + +public abstract interface class com/datadog/android/core/InternalSdkCore : com/datadog/android/api/feature/FeatureSdkCore { + public abstract fun deleteLastViewEvent ()V + public abstract fun getAllFeatures ()Ljava/util/List; + public abstract fun getAppStartTimeNs ()J + public abstract fun getDatadogContext (Ljava/util/Set;)Lcom/datadog/android/api/context/DatadogContext; + public abstract fun getFirstPartyHostResolver ()Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver; + public abstract fun getLastFatalAnrSent ()Ljava/lang/Long; + public abstract fun getLastViewEvent ()Lcom/google/gson/JsonObject; + public abstract fun getNetworkInfo ()Lcom/datadog/android/api/context/NetworkInfo; + public abstract fun getPersistenceExecutorService ()Ljava/util/concurrent/ExecutorService; + public abstract fun getRootStorageDir ()Ljava/io/File; + public abstract fun getTrackingConsent ()Lcom/datadog/android/privacy/TrackingConsent; + public abstract fun isDeveloperModeEnabled ()Z + public abstract fun writeLastFatalAnrSent (J)V + public abstract fun writeLastViewEvent ([B)V +} + +public final class com/datadog/android/core/InternalSdkCore$DefaultImpls { + public static synthetic fun getDatadogContext$default (Lcom/datadog/android/core/InternalSdkCore;Ljava/util/Set;ILjava/lang/Object;)Lcom/datadog/android/api/context/DatadogContext; +} + +public final class com/datadog/android/core/SdkReference { + public fun ()V + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun get ()Lcom/datadog/android/api/SdkCore; +} + +public final class com/datadog/android/core/UploadWorker : androidx/work/Worker { + public static final field Companion Lcom/datadog/android/core/UploadWorker$Companion; + public fun (Landroid/content/Context;Landroidx/work/WorkerParameters;)V + public fun doWork ()Landroidx/work/ListenableWorker$Result; +} + +public final class com/datadog/android/core/UploadWorker$Companion { +} + +public final class com/datadog/android/core/configuration/BackPressureMitigation : java/lang/Enum { + public static final field DROP_OLDEST Lcom/datadog/android/core/configuration/BackPressureMitigation; + public static final field IGNORE_NEWEST Lcom/datadog/android/core/configuration/BackPressureMitigation; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/core/configuration/BackPressureMitigation; + public static fun values ()[Lcom/datadog/android/core/configuration/BackPressureMitigation; +} + +public final class com/datadog/android/core/configuration/BackPressureStrategy { + public fun (ILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/configuration/BackPressureMitigation;)V + public final fun component1 ()I + public final fun component2 ()Lkotlin/jvm/functions/Function0; + public final fun component3 ()Lkotlin/jvm/functions/Function1; + public final fun component4 ()Lcom/datadog/android/core/configuration/BackPressureMitigation; + public final fun copy (ILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/configuration/BackPressureMitigation;)Lcom/datadog/android/core/configuration/BackPressureStrategy; + public static synthetic fun copy$default (Lcom/datadog/android/core/configuration/BackPressureStrategy;ILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/configuration/BackPressureMitigation;ILjava/lang/Object;)Lcom/datadog/android/core/configuration/BackPressureStrategy; + public fun equals (Ljava/lang/Object;)Z + public final fun getBackpressureMitigation ()Lcom/datadog/android/core/configuration/BackPressureMitigation; + public final fun getCapacity ()I + public final fun getOnItemDropped ()Lkotlin/jvm/functions/Function1; + public final fun getOnThresholdReached ()Lkotlin/jvm/functions/Function0; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/core/configuration/BatchProcessingLevel : java/lang/Enum { + public static final field HIGH Lcom/datadog/android/core/configuration/BatchProcessingLevel; + public static final field LOW Lcom/datadog/android/core/configuration/BatchProcessingLevel; + public static final field MEDIUM Lcom/datadog/android/core/configuration/BatchProcessingLevel; + public final fun getMaxBatchesPerUploadJob ()I + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/core/configuration/BatchProcessingLevel; + public static fun values ()[Lcom/datadog/android/core/configuration/BatchProcessingLevel; +} + +public final class com/datadog/android/core/configuration/BatchSize : java/lang/Enum { + public static final field LARGE Lcom/datadog/android/core/configuration/BatchSize; + public static final field MEDIUM Lcom/datadog/android/core/configuration/BatchSize; + public static final field SMALL Lcom/datadog/android/core/configuration/BatchSize; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/core/configuration/BatchSize; + public static fun values ()[Lcom/datadog/android/core/configuration/BatchSize; +} + +public final class com/datadog/android/core/configuration/Configuration { + public static final field Companion Lcom/datadog/android/core/configuration/Configuration$Companion; + public final fun copy (Lcom/datadog/android/core/configuration/Configuration$Core;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;)Lcom/datadog/android/core/configuration/Configuration; + public static synthetic fun copy$default (Lcom/datadog/android/core/configuration/Configuration;Lcom/datadog/android/core/configuration/Configuration$Core;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/core/configuration/Configuration; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/core/configuration/Configuration$Builder { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun build ()Lcom/datadog/android/core/configuration/Configuration; + public final fun setAdditionalConfiguration (Ljava/util/Map;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setBackpressureStrategy (Lcom/datadog/android/core/configuration/BackPressureStrategy;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setBatchProcessingLevel (Lcom/datadog/android/core/configuration/BatchProcessingLevel;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setBatchSize (Lcom/datadog/android/core/configuration/BatchSize;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setCrashReportsEnabled (Z)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setEncryption (Lcom/datadog/android/security/Encryption;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setFirstPartyHosts (Ljava/util/List;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setFirstPartyHostsWithHeaderType (Ljava/util/Map;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setPersistenceStrategyFactory (Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setProxy (Ljava/net/Proxy;Lokhttp3/Authenticator;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setUploadFrequency (Lcom/datadog/android/core/configuration/UploadFrequency;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setUploadSchedulerStrategy (Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setUseDeveloperModeWhenDebuggable (Z)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun useSite (Lcom/datadog/android/DatadogSite;)Lcom/datadog/android/core/configuration/Configuration$Builder; +} + +public final class com/datadog/android/core/configuration/Configuration$Companion { +} + +public final class com/datadog/android/core/configuration/HostsSanitizer { + public fun ()V + public final fun sanitizeHosts (Ljava/util/List;Ljava/lang/String;)Ljava/util/List; +} + +public final class com/datadog/android/core/configuration/UploadFrequency : java/lang/Enum { + public static final field AVERAGE Lcom/datadog/android/core/configuration/UploadFrequency; + public static final field FREQUENT Lcom/datadog/android/core/configuration/UploadFrequency; + public static final field RARE Lcom/datadog/android/core/configuration/UploadFrequency; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/core/configuration/UploadFrequency; + public static fun values ()[Lcom/datadog/android/core/configuration/UploadFrequency; +} + +public abstract interface class com/datadog/android/core/configuration/UploadSchedulerStrategy { + public abstract fun getMsDelayUntilNextUpload (Ljava/lang/String;ILjava/lang/Integer;Ljava/lang/Throwable;)J +} + +public abstract interface class com/datadog/android/core/constraints/DataConstraints { + public abstract fun validateAttributes (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)Ljava/util/Map; + public abstract fun validateTags (Ljava/util/List;)Ljava/util/List; + public abstract fun validateTimings (Ljava/util/Map;)Ljava/util/Map; +} + +public final class com/datadog/android/core/constraints/DataConstraints$DefaultImpls { + public static synthetic fun validateAttributes$default (Lcom/datadog/android/core/constraints/DataConstraints;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILjava/lang/Object;)Ljava/util/Map; +} + +public final class com/datadog/android/core/constraints/DatadogDataConstraints : com/datadog/android/core/constraints/DataConstraints { + public fun (Lcom/datadog/android/api/InternalLogger;)V + public fun validateAttributes (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)Ljava/util/Map; + public fun validateTags (Ljava/util/List;)Ljava/util/List; + public fun validateTimings (Ljava/util/Map;)Ljava/util/Map; +} + +public abstract class com/datadog/android/core/feature/event/JvmCrash { + public abstract fun getMessage ()Ljava/lang/String; + public abstract fun getThreads ()Ljava/util/List; + public abstract fun getThrowable ()Ljava/lang/Throwable; +} + +public final class com/datadog/android/core/feature/event/JvmCrash$Rum : com/datadog/android/core/feature/event/JvmCrash { + public fun (Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/List;)V + public final fun component1 ()Ljava/lang/Throwable; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun copy (Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/List;)Lcom/datadog/android/core/feature/event/JvmCrash$Rum; + public static synthetic fun copy$default (Lcom/datadog/android/core/feature/event/JvmCrash$Rum;Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/core/feature/event/JvmCrash$Rum; + public fun equals (Ljava/lang/Object;)Z + public fun getMessage ()Ljava/lang/String; + public fun getThreads ()Ljava/util/List; + public fun getThrowable ()Ljava/lang/Throwable; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/core/feature/event/ThreadDump { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Z + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)Lcom/datadog/android/core/feature/event/ThreadDump; + public static synthetic fun copy$default (Lcom/datadog/android/core/feature/event/ThreadDump;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lcom/datadog/android/core/feature/event/ThreadDump; + public fun equals (Ljava/lang/Object;)Z + public final fun getCrashed ()Z + public final fun getName ()Ljava/lang/String; + public final fun getStack ()Ljava/lang/String; + public final fun getState ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver : com/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver { + public fun (Ljava/util/Map;)V + public fun getAllHeaderTypes ()Ljava/util/Set; + public fun headerTypesForUrl (Ljava/lang/String;)Ljava/util/Set; + public fun headerTypesForUrl (Lokhttp3/HttpUrl;)Ljava/util/Set; + public fun isEmpty ()Z + public fun isFirstPartyUrl (Ljava/lang/String;)Z + public fun isFirstPartyUrl (Lokhttp3/HttpUrl;)Z +} + +public abstract interface class com/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver { + public abstract fun getAllHeaderTypes ()Ljava/util/Set; + public abstract fun headerTypesForUrl (Ljava/lang/String;)Ljava/util/Set; + public abstract fun headerTypesForUrl (Lokhttp3/HttpUrl;)Ljava/util/Set; + public abstract fun isEmpty ()Z + public abstract fun isFirstPartyUrl (Ljava/lang/String;)Z + public abstract fun isFirstPartyUrl (Lokhttp3/HttpUrl;)Z +} + +public abstract interface class com/datadog/android/core/internal/persistence/Deserializer { + public abstract fun deserialize (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class com/datadog/android/core/internal/persistence/file/FileExtKt { + public static final fun canReadSafe (Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z + public static final fun existsSafe (Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z + public static final fun listFilesSafe (Ljava/io/File;Lcom/datadog/android/api/InternalLogger;Ljava/io/FilenameFilter;)[Ljava/io/File; + public static final fun readLinesSafe (Ljava/io/File;Ljava/nio/charset/Charset;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List; + public static synthetic fun readLinesSafe$default (Ljava/io/File;Ljava/nio/charset/Charset;Lcom/datadog/android/api/InternalLogger;ILjava/lang/Object;)Ljava/util/List; + public static final fun readTextSafe (Ljava/io/File;Ljava/nio/charset/Charset;Lcom/datadog/android/api/InternalLogger;)Ljava/lang/String; + public static synthetic fun readTextSafe$default (Ljava/io/File;Ljava/nio/charset/Charset;Lcom/datadog/android/api/InternalLogger;ILjava/lang/Object;)Ljava/lang/String; +} + +public abstract interface class com/datadog/android/core/internal/system/BuildSdkVersionProvider { + public static final field Companion Lcom/datadog/android/core/internal/system/BuildSdkVersionProvider$Companion; + public abstract fun getVersion ()I +} + +public final class com/datadog/android/core/internal/system/BuildSdkVersionProvider$Companion { + public final fun getDEFAULT ()Lcom/datadog/android/core/internal/system/BuildSdkVersionProvider; +} + +public final class com/datadog/android/core/internal/utils/ByteArrayExtKt { + public static final fun join (Ljava/util/Collection;[B[B[BLcom/datadog/android/api/InternalLogger;)[B + public static synthetic fun join$default (Ljava/util/Collection;[B[B[BLcom/datadog/android/api/InternalLogger;ILjava/lang/Object;)[B +} + +public final class com/datadog/android/core/internal/utils/ConcurrencyExtKt { + public static final fun executeSafe (Ljava/util/concurrent/Executor;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;Ljava/lang/Runnable;)V + public static final fun getSafe (Ljava/util/concurrent/Future;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;)Ljava/lang/Object; + public static final fun scheduleSafe (Ljava/util/concurrent/ScheduledExecutorService;Ljava/lang/String;JLjava/util/concurrent/TimeUnit;Lcom/datadog/android/api/InternalLogger;Ljava/lang/Runnable;)Ljava/util/concurrent/ScheduledFuture; + public static final fun submitSafe (Ljava/util/concurrent/ExecutorService;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;Ljava/lang/Runnable;)Ljava/util/concurrent/Future; + public static final fun submitSafe (Ljava/util/concurrent/ExecutorService;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future; +} + +public final class com/datadog/android/core/internal/utils/JsonSerializer { + public static final field INSTANCE Lcom/datadog/android/core/internal/utils/JsonSerializer; + public final fun safeMapValuesToJson (Ljava/util/Map;Lcom/datadog/android/api/InternalLogger;)Ljava/util/Map; + public final fun toJsonElement (Ljava/lang/Object;)Lcom/google/gson/JsonElement; +} + +public final class com/datadog/android/core/metrics/MethodCallSamplingRate : java/lang/Enum { + public static final field ALL Lcom/datadog/android/core/metrics/MethodCallSamplingRate; + public static final field HIGH Lcom/datadog/android/core/metrics/MethodCallSamplingRate; + public static final field LOW Lcom/datadog/android/core/metrics/MethodCallSamplingRate; + public static final field MEDIUM Lcom/datadog/android/core/metrics/MethodCallSamplingRate; + public static final field RARE Lcom/datadog/android/core/metrics/MethodCallSamplingRate; + public static final field REDUCED Lcom/datadog/android/core/metrics/MethodCallSamplingRate; + public final fun getRate ()F + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/core/metrics/MethodCallSamplingRate; + public static fun values ()[Lcom/datadog/android/core/metrics/MethodCallSamplingRate; +} + +public abstract interface class com/datadog/android/core/metrics/PerformanceMetric { + public static final field Companion Lcom/datadog/android/core/metrics/PerformanceMetric$Companion; + public static final field METRIC_TYPE Ljava/lang/String; + public abstract fun stopAndSend (Z)V +} + +public final class com/datadog/android/core/metrics/PerformanceMetric$Companion { + public static final field METRIC_TYPE Ljava/lang/String; +} + +public final class com/datadog/android/core/metrics/TelemetryMetricType : java/lang/Enum { + public static final field MethodCalled Lcom/datadog/android/core/metrics/TelemetryMetricType; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/core/metrics/TelemetryMetricType; + public static fun values ()[Lcom/datadog/android/core/metrics/TelemetryMetricType; +} + +public abstract interface class com/datadog/android/core/persistence/PersistenceStrategy { + public abstract fun currentMetadata ()[B + public abstract fun dropAll ()V + public abstract fun lockAndReadNext ()Lcom/datadog/android/core/persistence/PersistenceStrategy$Batch; + public abstract fun migrateData (Lcom/datadog/android/core/persistence/PersistenceStrategy;)V + public abstract fun unlockAndDelete (Ljava/lang/String;)V + public abstract fun unlockAndKeep (Ljava/lang/String;)V + public abstract fun write (Lcom/datadog/android/api/storage/RawBatchEvent;[BLcom/datadog/android/api/storage/EventType;)Z +} + +public final class com/datadog/android/core/persistence/PersistenceStrategy$Batch { + public fun (Ljava/lang/String;[BLjava/util/List;)V + public synthetic fun (Ljava/lang/String;[BLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()[B + public final fun component3 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;[BLjava/util/List;)Lcom/datadog/android/core/persistence/PersistenceStrategy$Batch; + public static synthetic fun copy$default (Lcom/datadog/android/core/persistence/PersistenceStrategy$Batch;Ljava/lang/String;[BLjava/util/List;ILjava/lang/Object;)Lcom/datadog/android/core/persistence/PersistenceStrategy$Batch; + public fun equals (Ljava/lang/Object;)Z + public final fun getBatchId ()Ljava/lang/String; + public final fun getEvents ()Ljava/util/List; + public final fun getMetadata ()[B + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/datadog/android/core/persistence/PersistenceStrategy$Factory { + public abstract fun create (Ljava/lang/String;IJ)Lcom/datadog/android/core/persistence/PersistenceStrategy; +} + +public abstract interface class com/datadog/android/core/persistence/Serializer { + public static final field Companion Lcom/datadog/android/core/persistence/Serializer$Companion; + public abstract fun serialize (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class com/datadog/android/core/persistence/Serializer$Companion { +} + +public final class com/datadog/android/core/persistence/SerializerKt { + public static final fun serializeToByteArray (Lcom/datadog/android/core/persistence/Serializer;Ljava/lang/Object;Lcom/datadog/android/api/InternalLogger;)[B +} + +public final class com/datadog/android/core/persistence/datastore/DataStoreContent { + public fun (ILjava/lang/Object;)V + public final fun component1 ()I + public final fun component2 ()Ljava/lang/Object; + public final fun copy (ILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; + public static synthetic fun copy$default (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;ILjava/lang/Object;ILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Ljava/lang/Object; + public final fun getVersionCode ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class com/datadog/android/core/sampling/DeterministicSampler : com/datadog/android/core/sampling/Sampler { + public static final field Companion Lcom/datadog/android/core/sampling/DeterministicSampler$Companion; + public static final field MAX_ID J + public static final field SAMPLER_HASHER J + public static final field SAMPLE_ALL_RATE F + public fun (Lkotlin/jvm/functions/Function1;D)V + public fun (Lkotlin/jvm/functions/Function1;F)V + public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V + public fun getSampleRate ()Ljava/lang/Float; + public fun sample (Ljava/lang/Object;)Z +} + +public final class com/datadog/android/core/sampling/DeterministicSampler$Companion { +} + +public class com/datadog/android/core/sampling/RateBasedSampler : com/datadog/android/core/sampling/Sampler { + public static final field SAMPLE_ALL_RATE F + public fun (D)V + public fun (F)V + public fun (Lkotlin/jvm/functions/Function0;)V + public fun getSampleRate ()Ljava/lang/Float; + public fun sample (Ljava/lang/Object;)Z +} + +public abstract interface class com/datadog/android/core/sampling/Sampler { + public abstract fun getSampleRate ()Ljava/lang/Float; + public abstract fun sample (Ljava/lang/Object;)Z +} + +public abstract interface class com/datadog/android/core/thread/FlushableExecutorService : java/util/concurrent/ExecutorService { + public abstract fun drainTo (Ljava/util/Collection;)V +} + +public abstract interface class com/datadog/android/core/thread/FlushableExecutorService$Factory { + public abstract fun create (Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;)Lcom/datadog/android/core/thread/FlushableExecutorService; +} + +public abstract interface class com/datadog/android/event/EventMapper { + public abstract fun map (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class com/datadog/android/event/MapperSerializer : com/datadog/android/core/persistence/Serializer { + public fun (Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/core/persistence/Serializer;)V + public fun serialize (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class com/datadog/android/event/NoOpEventMapper : com/datadog/android/event/EventMapper { + public fun ()V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun map (Ljava/lang/Object;)Ljava/lang/Object; +} + +public abstract interface annotation class com/datadog/android/lint/InternalApi : java/lang/annotation/Annotation { +} + +public final class com/datadog/android/log/LogAttributes { + public static final field ACCOUNT_ATTRIBUTES_GROUP Ljava/lang/String; + public static final field ACCOUNT_ID Ljava/lang/String; + public static final field ACCOUNT_NAME Ljava/lang/String; + public static final field APPLICATION_PACKAGE Ljava/lang/String; + public static final field APPLICATION_VERSION Ljava/lang/String; + public static final field DATE Ljava/lang/String; + public static final field DB_INSTANCE Ljava/lang/String; + public static final field DB_OPERATION Ljava/lang/String; + public static final field DB_STATEMENT Ljava/lang/String; + public static final field DB_USER Ljava/lang/String; + public static final field DD_SPAN_ID Ljava/lang/String; + public static final field DD_TRACE_ID Ljava/lang/String; + public static final field DURATION Ljava/lang/String; + public static final field ENV Ljava/lang/String; + public static final field ERROR_FINGERPRINT Ljava/lang/String; + public static final field ERROR_KIND Ljava/lang/String; + public static final field ERROR_MESSAGE Ljava/lang/String; + public static final field ERROR_SOURCE_TYPE Ljava/lang/String; + public static final field ERROR_STACK Ljava/lang/String; + public static final field HOST Ljava/lang/String; + public static final field HTTP_METHOD Ljava/lang/String; + public static final field HTTP_REFERRER Ljava/lang/String; + public static final field HTTP_REQUEST_ID Ljava/lang/String; + public static final field HTTP_STATUS_CODE Ljava/lang/String; + public static final field HTTP_URL Ljava/lang/String; + public static final field HTTP_USERAGENT Ljava/lang/String; + public static final field HTTP_VERSION Ljava/lang/String; + public static final field INSTANCE Lcom/datadog/android/log/LogAttributes; + public static final field LOGGER_METHOD_NAME Ljava/lang/String; + public static final field LOGGER_NAME Ljava/lang/String; + public static final field LOGGER_THREAD_NAME Ljava/lang/String; + public static final field LOGGER_VERSION Ljava/lang/String; + public static final field MESSAGE Ljava/lang/String; + public static final field NETWORK_CARRIER_ID Ljava/lang/String; + public static final field NETWORK_CARRIER_NAME Ljava/lang/String; + public static final field NETWORK_CLIENT_IP Ljava/lang/String; + public static final field NETWORK_CLIENT_PORT Ljava/lang/String; + public static final field NETWORK_CONNECTIVITY Ljava/lang/String; + public static final field NETWORK_DOWN_KBPS Ljava/lang/String; + public static final field NETWORK_SIGNAL_STRENGTH Ljava/lang/String; + public static final field NETWORK_UP_KBPS Ljava/lang/String; + public static final field RUM_ACTION_ID Ljava/lang/String; + public static final field RUM_APPLICATION_ID Ljava/lang/String; + public static final field RUM_SESSION_ID Ljava/lang/String; + public static final field RUM_VIEW_ID Ljava/lang/String; + public static final field SERVICE_NAME Ljava/lang/String; + public static final field SOURCE Ljava/lang/String; + public static final field SOURCE_TYPE Ljava/lang/String; + public static final field STATUS Ljava/lang/String; + public static final field USR_ATTRIBUTES_GROUP Ljava/lang/String; + public static final field USR_EMAIL Ljava/lang/String; + public static final field USR_ID Ljava/lang/String; + public static final field USR_NAME Ljava/lang/String; + public static final field VARIANT Ljava/lang/String; +} + +public final class com/datadog/android/privacy/TrackingConsent : java/lang/Enum { + public static final field GRANTED Lcom/datadog/android/privacy/TrackingConsent; + public static final field NOT_GRANTED Lcom/datadog/android/privacy/TrackingConsent; + public static final field PENDING Lcom/datadog/android/privacy/TrackingConsent; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/privacy/TrackingConsent; + public static fun values ()[Lcom/datadog/android/privacy/TrackingConsent; +} + +public abstract interface class com/datadog/android/privacy/TrackingConsentProviderCallback { + public abstract fun onConsentUpdated (Lcom/datadog/android/privacy/TrackingConsent;Lcom/datadog/android/privacy/TrackingConsent;)V +} + +public abstract interface class com/datadog/android/security/Encryption { + public abstract fun decrypt ([B)[B + public abstract fun encrypt ([B)[B +} + +public final class com/datadog/android/trace/TracingHeaderType : java/lang/Enum { + public static final field B3 Lcom/datadog/android/trace/TracingHeaderType; + public static final field B3MULTI Lcom/datadog/android/trace/TracingHeaderType; + public static final field DATADOG Lcom/datadog/android/trace/TracingHeaderType; + public static final field TRACECONTEXT Lcom/datadog/android/trace/TracingHeaderType; + public final fun getHeaderType ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/trace/TracingHeaderType; + public static fun values ()[Lcom/datadog/android/trace/TracingHeaderType; +} + diff --git a/dd-sdk-android-core/build.gradle.kts b/dd-sdk-android-core/build.gradle.kts new file mode 100644 index 0000000000..51a0e1667d --- /dev/null +++ b/dd-sdk-android-core/build.gradle.kts @@ -0,0 +1,156 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import com.datadog.gradle.config.AndroidConfig +import com.datadog.gradle.config.BuildConfigPropertiesKeys +import com.datadog.gradle.config.GradlePropertiesKeys +import com.datadog.gradle.config.androidLibraryConfig +import com.datadog.gradle.config.dependencyUpdateConfig +import com.datadog.gradle.config.detektCustomConfig +import com.datadog.gradle.config.javadocConfig +import com.datadog.gradle.config.junitConfig +import com.datadog.gradle.config.kotlinConfig +import com.datadog.gradle.config.publishingConfig +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + // Build + id("com.android.library") + kotlin("android") + id("com.google.devtools.ksp") + + // Publish + `maven-publish` + signing + id("org.jetbrains.dokka-javadoc") + + // Analysis tools + id("com.github.ben-manes.versions") + + // Tests + id("de.mobilej.unmock") + id("org.jetbrains.kotlinx.kover") + + // Internal Generation + id("com.datadoghq.dependency-license") + id("apiSurface") + id("transitiveDependencies") + id("verificationXml") + id("binary-compatibility-validator") +} + +/** + * Checks whether logcat logs should be enabled when building the release version of the library. + * @return true if logcat logs should be enabled + */ +fun isLogEnabledInRelease(): String { + return project.findProperty(GradlePropertiesKeys.FORCE_ENABLE_LOGCAT) as? String ?: "false" +} + +android { + defaultConfig { + buildFeatures { + buildConfig = true + } + buildConfigField( + "int", + "SDK_VERSION_CODE", + "${AndroidConfig.VERSION.code}" + ) + buildConfigField( + "String", + "SDK_VERSION_NAME", + "\"${AndroidConfig.VERSION.name}\"" + ) + } + + namespace = "com.datadog.android" + + buildTypes { + getByName("release") { + buildConfigField( + "Boolean", + BuildConfigPropertiesKeys.LOGCAT_ENABLED, + isLogEnabledInRelease() + ) + } + + getByName("debug") { + buildConfigField( + "Boolean", + BuildConfigPropertiesKeys.LOGCAT_ENABLED, + "true" + ) + } + } + + testFixtures { + enable = true + } +} + +dependencies { + implementation(libs.kotlin) + + // Network + implementation(libs.okHttp) + implementation(libs.gson) + implementation(libs.kronosNTP) + + // Android Instrumentation + implementation(libs.androidXAnnotation) + implementation(libs.androidXCollection) + implementation(libs.androidXWorkManager) + + implementation(project(":dd-sdk-android-internal")) + + // Generate NoOp implementations + ksp(project(":tools:noopfactory")) + + // Lint rules + lintPublish(project(":tools:lint")) + + // Testing + testImplementation(project(":tools:unit")) { + attributes { + attribute( + com.android.build.api.attributes.ProductFlavorAttr.of("platform"), + objects.named("jvm") + ) + } + } + testImplementation(testFixtures(project(":dd-sdk-android-internal"))) + testImplementation(libs.bundles.jUnit5) + testImplementation(libs.bundles.testTools) + unmock(libs.robolectric) + + // Test Fixtures + testFixturesImplementation(libs.kotlin) + testFixturesImplementation(libs.bundles.jUnit5) + testFixturesImplementation(libs.okHttp) + testFixturesImplementation(libs.bundles.testTools) +} + +unMock { + keep("android.os.BaseBundle") + keep("android.os.Bundle") + keep("android.os.Parcel") + keepStartingWith("com.android.internal.util.") + keepStartingWith("android.util.") + keep("android.content.ComponentName") + keep("android.os.Looper") + keep("android.os.MessageQueue") + keep("android.os.SystemProperties") + keepStartingWith("org.json") +} + +kotlinConfig(jvmBytecodeTarget = JvmTarget.JVM_11) +androidLibraryConfig() +junitConfig() +javadocConfig() +dependencyUpdateConfig() +publishingConfig("Datadog monitoring library for Android applications.") +detektCustomConfig() diff --git a/dd-sdk-android-core/src/main/AndroidManifest.xml b/dd-sdk-android-core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..65e6feb363 --- /dev/null +++ b/dd-sdk-android-core/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/Datadog.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/Datadog.kt new file mode 100644 index 0000000000..afb87766f9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/Datadog.kt @@ -0,0 +1,454 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android + +import android.content.Context +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.Datadog.clearAccountInfo +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.context.UserInfo +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.internal.DatadogCore +import com.datadog.android.core.internal.HashGenerator +import com.datadog.android.core.internal.NoOpInternalSdkCore +import com.datadog.android.core.internal.SdkCoreRegistry +import com.datadog.android.core.internal.Sha256HashGenerator +import com.datadog.android.core.internal.utils.unboundInternalLogger +import com.datadog.android.internal.utils.loggableStackTrace +import com.datadog.android.lint.InternalApi +import com.datadog.android.privacy.TrackingConsent +import java.util.Locale + +/** + * This class initializes the Datadog SDK, and sets up communication with the server. + */ +@Suppress("TooManyFunctions") +object Datadog { + + internal val registry = SdkCoreRegistry(unboundInternalLogger) + + internal var hashGenerator: HashGenerator = Sha256HashGenerator() + + internal var libraryVerbosity = Int.MAX_VALUE + + // region Initialization + + /** + * Initializes a named instance of the Datadog SDK. + * @param instanceName the name of the instance (or null to initialize the default instance). + * Note that the instance name should be stable across builds. + * @param context your application context + * @param configuration the configuration for the SDK library + * @param trackingConsent as the initial state of the tracking consent flag + * @return the initialized SDK instance, or null if something prevents the SDK from + * being initialized + * @see [Configuration] + * @see [TrackingConsent] + * @throws IllegalArgumentException if the env name is using illegal characters and your + * application is in debug mode otherwise returns null and stops initializing the SDK + */ + @Suppress("ReturnCount") + @JvmStatic + fun initialize( + instanceName: String?, + context: Context, + configuration: Configuration, + trackingConsent: TrackingConsent + ): SdkCore? { + synchronized(registry) { + val existing = registry.getInstance(instanceName) + if (existing != null) { + unboundInternalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { MESSAGE_ALREADY_INITIALIZED } + ) + return existing + } + + val sdkInstanceId = hashGenerator.generate( + "$instanceName/${configuration.coreConfig.site.siteName}" + ) + + if (sdkInstanceId == null) { + unboundInternalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { CANNOT_CREATE_SDK_INSTANCE_ID_ERROR } + ) + return null + } + + val sdkInstanceName = instanceName ?: SdkCoreRegistry.DEFAULT_INSTANCE_NAME + val sdkCore = DatadogCore( + context, + sdkInstanceId, + sdkInstanceName + ).apply { + initialize(configuration) + // not pushing to the context thread to have it set already at the + // moment Datadog.initialize is completed + coreFeature.trackingConsentProvider.setConsent(trackingConsent) + } + registry.register(sdkInstanceName, sdkCore) + + return sdkCore + } + } + + /** + * Initializes the Datadog SDK. + * @param context your application context + * @param configuration the configuration for the SDK library + * @param trackingConsent as the initial state of the tracking consent flag + * @return the initialized SDK instance, or null if something prevents the SDK from + * being initialized + * @see [Configuration] + * @see [TrackingConsent] + * @throws IllegalArgumentException if the env name is using illegal characters and your + * application is in debug mode otherwise returns null and stops initializing the SDK + */ + @JvmStatic + fun initialize( + context: Context, + configuration: Configuration, + trackingConsent: TrackingConsent + ): SdkCore? { + return initialize(null, context, configuration, trackingConsent) + } + + /** + * Retrieve the initialized SDK instance attached to the given name, + * or the default instance if the name is null. + * @param instanceName the name of the instance to retrieve, + * or null to get the default instance + * @return the existing instance linked with the given name, or no-op instance if instance + * with given name is not yet initialized. + */ + @JvmStatic + @JvmOverloads + fun getInstance(instanceName: String? = null): SdkCore { + return synchronized(registry) { + val sdkInstanceName = instanceName ?: SdkCoreRegistry.DEFAULT_INSTANCE_NAME + val sdkInstance = registry.getInstance(sdkInstanceName) + if (sdkInstance == null) { + @Suppress("ThrowingExceptionsWithoutMessageOrCause") + val stackCapture = Throwable().fillInStackTrace() + unboundInternalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { + MESSAGE_SDK_NOT_INITIALIZED.format( + Locale.US, + sdkInstanceName, + stackCapture + .loggableStackTrace() + .lines() + .drop(1) + .joinToString(separator = "\n") + ) + } + ) + NoOpInternalSdkCore + } else { + sdkInstance + } + } + } + + /** + * Checks if SDK instance with a given name is initialized. + * @param instanceName the name of the instance to retrieve, + * or null to check the default instance + * @return whenever the instance with given name is initialized or not. + */ + @JvmStatic + @JvmOverloads + fun isInitialized(instanceName: String? = null): Boolean { + return synchronized(registry) { + registry.getInstance(instanceName) != null + } + } + + /** + * Stop the initialized SDK instance attached to the given name, + * or the default instance if the name is null. + * @param instanceName the name of the instance to stop, + * or null to stop the default instance + */ + @JvmStatic + @JvmOverloads + fun stopInstance(instanceName: String? = null) { + synchronized(registry) { + val instance = registry.unregister(instanceName) + (instance as? DatadogCore)?.stop() + } + } + + // endregion + + // region Global methods + + /** + * Sets the verbosity of this instance of the Datadog SDK. + * + * Messages with a priority level equal or above the given level will be sent to Android's + * Logcat. + * + * @param level one of the Android [android.util.Log] constants + * ([android.util.Log.VERBOSE], [android.util.Log.DEBUG], [android.util.Log.INFO], + * [android.util.Log.WARN], [android.util.Log.ERROR], [android.util.Log.ASSERT]). + */ + @JvmStatic + fun setVerbosity(level: Int) { + libraryVerbosity = level + } + + /** + * Gets the verbosity of this instance of the Datadog SDK. + * + * Messages with a priority level equal or above the given level will be sent to Android's + * Logcat. + * + * @returns level one of the Android [android.util.Log] constants + * ([android.util.Log.VERBOSE], [android.util.Log.DEBUG], [android.util.Log.INFO], + * [android.util.Log.WARN], [android.util.Log.ERROR], [android.util.Log.ASSERT]). + */ + @JvmStatic + fun getVerbosity(): Int = libraryVerbosity + + /** + * Sets the tracking consent regarding the data collection for this instance of the Datadog SDK. + * + * @param consent which can take one of the values + * ([TrackingConsent.PENDING], [TrackingConsent.GRANTED], [TrackingConsent.NOT_GRANTED]) + * @param sdkCore SDK instance to set tracking consent in. If not provided, default SDK instance + * will be used. + */ + @JvmStatic + @JvmOverloads + fun setTrackingConsent(consent: TrackingConsent, sdkCore: SdkCore = getInstance()) { + sdkCore.setTrackingConsent(consent) + } + + /** + * Sets the user information. + * + * @param id a unique user identifier (relevant to your business domain) + * @param name (nullable) the user name or alias + * @param email (nullable) the user email + * @param extraInfo additional information. An extra information can be + * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. + * @param sdkCore SDK instance to set user info in. If not provided, default SDK instance + * will be used. + */ + @JvmStatic + @JvmOverloads + fun setUserInfo( + id: String, + name: String? = null, + email: String? = null, + extraInfo: Map = emptyMap(), + sdkCore: SdkCore = getInstance() + ) { + sdkCore.setUserInfo(id, name, email, extraInfo) + } + + /** + * Sets additional information on the [UserInfo] object + * + * If properties had originally been set with [SdkCore.setUserInfo], they will be preserved. + * In the event of a conflict on key, the new property will prevail. + * + * @param extraInfo additional information. An extra information can be + * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. + * @param sdkCore SDK instance to add user properties. If not provided, default SDK instance + * will be used. + */ + @JvmStatic + @JvmOverloads + fun addUserProperties(extraInfo: Map, sdkCore: SdkCore = getInstance()) { + sdkCore.addUserProperties(extraInfo) + } + + /** + * Clear the current user information. + * + * User information will be set to null. + * After calling this api, Logs, Traces, RUM Events will not include the user information anymore. + * + * Any active RUM Session, active RUM View at the time of call will have their `usr` attribute cleared. + * + * If you want to retain the current `usr` on the active RUM session, + * you need to stop the session first by using `GlobalRumMonitor.get().stopSession()` + * + * If you want to retain the current `usr` on the active RUM views, + * you need to stop the view first by using `GlobalRumMonitor.get().stopView()`. + * + * @param sdkCore SDK instance to clear user info. If not provided, + * default SDK instance will be used. + */ + @JvmStatic + @JvmOverloads + @AnyThread + fun clearUserInfo( + sdkCore: SdkCore = getInstance() + ) { + sdkCore.clearUserInfo() + } + + /** + * Clears all unsent data in all registered features. + * + * @param sdkCore SDK instance to clear the data. If not provided, default SDK instance + * will be used. + */ + @JvmStatic + @JvmOverloads + @AnyThread + fun clearAllData(sdkCore: SdkCore = getInstance()) { + sdkCore.clearAllData() + } + + /** + * Sets the account information that the user is currently logged into. + * + * This API should be used to assign an identifier for the user's account which represents a + * contextual identity within the app, typically tied to business or tenant logic. The + * information set here will be added to logs, traces and RUM events. + * + * This value should be set when user logs in with his account, and cleared by calling + * [clearAccountInfo] when he logs out. + * + * @param id Account ID. + * @param name representing the account, if exists. + * @param extraInfo Account custom attributes, if exists. + * @param sdkCore SDK instance to set account information. If not provided, default SDK + * instance will be used. + */ + @JvmStatic + @JvmOverloads + fun setAccountInfo( + id: String, + name: String? = null, + extraInfo: Map = emptyMap(), + sdkCore: SdkCore = getInstance() + ) { + sdkCore.setAccountInfo( + id = id, + name = name, + extraInfo = extraInfo + ) + } + + /** + * Add custom attributes to the current account information. + * + * This extra info will be added to already existing extra info that is added + * to Logs, Traces and RUM events automatically. + * + * @param extraInfo Account additional custom attributes. + * @param sdkCore SDK instance to add extra account information. If not provided, default SDK + * instance will be used. + */ + @JvmStatic + @JvmOverloads + fun addAccountExtraInfo( + extraInfo: Map, + sdkCore: SdkCore = getInstance() + ) { + sdkCore.addAccountExtraInfo(extraInfo) + } + + /** + * Clear the current account information. + * + * Account information will be set to null. + * Following Logs, Traces, RUM Events will not include the account information anymore. + * + * Any active RUM Session, active RUM View at the time of call will have their `account` attribute cleared. + * + * If you want to retain the current `account` on the active RUM session, + * you need to stop the session first by using `GlobalRumMonitor.get().stopSession()`. + * + * If you want to retain the current `account` on the active RUM views, + * you need to stop the view first by using `GlobalRumMonitor.get().stopView()`. + * + * @param sdkCore SDK instance to clear account information. If not provided, default SDK + * instance will be used. + */ + @JvmStatic + @JvmOverloads + fun clearAccountInfo( + sdkCore: SdkCore = getInstance() + ) { + sdkCore.clearAccountInfo() + } + + // Executes all the pending queues in the upload/persistence executors. + // Tries to send all the granted data for each feature and then clears the folders and shuts + // down the persistence and the upload executors. + // You should not use this method in production code. By calling this method you will basically + // stop the SDKs persistence - upload streams and will leave it in an inconsistent state. This + // method is mainly for test purposes. + @Suppress("unused") + @WorkerThread + private fun flushAndShutdownExecutors() { + // Note for the future: if we decide to make this a public feature, + // we need to drain, execute and flush from a background thread or ensure we're + // not in the main thread! + synchronized(registry) { + val sdkCore = registry.getInstance() as? FeatureSdkCore + if (sdkCore != null) { + sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + ?.sendEvent( + mapOf( + "type" to "flush_and_stop_monitor" + ) + ) + (sdkCore as? DatadogCore)?.flushStoredData() + } + } + } + + /** + * For Datadog internal use only. + * + * @see _InternalProxy + */ + @InternalApi + @Suppress("FunctionNaming", "FunctionName") + fun _internalProxy(instanceName: String? = null): _InternalProxy { + return _InternalProxy(getInstance(instanceName)) + } + + // endregion + + // region Constants + + internal const val MESSAGE_ALREADY_INITIALIZED = + "The Datadog library has already been initialized." + + internal const val MESSAGE_SDK_NOT_INITIALIZED = "SDK instance with name %s is not found," + + " returning no-op implementation. Please make sure to call" + + " Datadog.initialize([instanceName]) before getting the instance." + + " SDK instance was requested from:\n%s" + + internal const val CANNOT_CREATE_SDK_INSTANCE_ID_ERROR = + "Cannot create SDK instance ID, stopping SDK initialization." + + internal const val DD_SOURCE_TAG = "_dd.source" + internal const val DD_SDK_VERSION_TAG = "_dd.sdk_version" + internal const val DD_APP_VERSION_TAG = "_dd.version" + internal const val DD_NATIVE_SOURCE_TYPE = "_dd.native_source_type" + + // endregion +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/DatadogSite.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/DatadogSite.kt new file mode 100644 index 0000000000..26b4c2731b --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/DatadogSite.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android + +/** + * Defines the Datadog sites you can send tracked data to. + * + * @param siteName Explicit site name property introduced in order to have a consistent SDK + * instance ID (because this value is used there) in case if enum values are renamed. + * @param intakeHostName the host name for the given site. + */ +enum class DatadogSite private constructor(internal val siteName: String, private val intakeHostName: String) { + + /** + * The US1 site: [app.datadoghq.com](https://app.datadoghq.com). + */ + US1("us1", "browser-intake-datadoghq.com"), + + /** + * The US3 site: [us3.datadoghq.com](https://us3.datadoghq.com). + */ + US3("us3"), + + /** + * The US5 site: [us5.datadoghq.com](https://us5.datadoghq.com). + */ + US5("us5"), + + /** + * The EU1 site: [app.datadoghq.eu](https://app.datadoghq.eu). + */ + EU1("eu1", "browser-intake-datadoghq.eu"), + + /** + * The AP1 site: [ap1.datadoghq.com](https://ap1.datadoghq.com). + */ + AP1("ap1"), + + /** + * The AP2 site: [ap2.datadoghq.com](https://ap2.datadoghq.com). + */ + AP2("ap2"), + + /** + * The US1_FED site (FedRAMP compatible): [app.ddog-gov.com](https://app.ddog-gov.com). + */ + US1_FED("us1_fed", "browser-intake-ddog-gov.com"), + + /** + * The STAGING site (internal usage only): [app.datad0g.com](https://app.datad0g.com). + */ + STAGING("staging", "browser-intake-datad0g.com"); + + /** + * Constructor using the generic way to build the intake endpoint host from the site name. + * @param siteName Explicit site name property introduced in order to have a consistent SDK + * instance ID (because this value is used there) in case if enum values are renamed. + */ + private constructor(siteName: String) : this( + siteName, + "browser-intake-$siteName-datadoghq.com" + ) + + /** The intake endpoint url. */ + val intakeEndpoint: String = "https://$intakehostname/" +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/_InternalProxy.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/_InternalProxy.kt new file mode 100644 index 0000000000..9797bcdd4b --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/_InternalProxy.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android + +import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.internal.DatadogCore +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.android.lint.InternalApi + +/** + * This class 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 class 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. + */ +@InternalApi +@Suppress( + "UndocumentedPublicClass", + "UndocumentedPublicFunction", + "UndocumentedPublicProperty", + "ClassName", + "ClassNaming", + "VariableNaming" +) +class _InternalProxy internal constructor( + private val sdkCore: SdkCore +) { + @Suppress("StringLiteralDuplication") + class _TelemetryProxy internal constructor(private val sdkCore: SdkCore) { + + private val rumFeature: FeatureScope? + get() { + return (sdkCore as? FeatureSdkCore)?.getFeature(Feature.RUM_FEATURE_NAME) + } + + fun debug(message: String) { + val telemetryEvent = InternalTelemetryEvent.Log.Debug( + message = message, + additionalProperties = null + ) + rumFeature?.sendEvent(telemetryEvent) + } + + fun error(message: String, throwable: Throwable? = null) { + val telemetryEvent = InternalTelemetryEvent.Log.Error( + message = message, + error = throwable + ) + rumFeature?.sendEvent(telemetryEvent) + } + + fun error(message: String, stack: String?, kind: String?) { + val telemetryEvent = InternalTelemetryEvent.Log.Error( + message = message, + stacktrace = stack, + kind = kind + ) + rumFeature?.sendEvent(telemetryEvent) + } + } + + @Suppress("PropertyName") + val _telemetry: _TelemetryProxy = _TelemetryProxy(sdkCore) + + fun setCustomAppVersion(version: String) { + val coreFeature = (sdkCore as? DatadogCore)?.coreFeature + coreFeature?.packageVersionProvider?.version = version + } + + companion object { + // TODO RUM-368 Expose it as public API? Needed for the integration tests at least, + // because OkHttp MockWebServer is HTTP based + fun allowClearTextHttp(builder: Configuration.Builder): Configuration.Builder { + return builder.allowClearTextHttp() + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt new file mode 100644 index 0000000000..4ac79482dd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api + +import androidx.annotation.FloatRange +import com.datadog.android.core.internal.logger.SdkInternalLogger +import com.datadog.android.core.metrics.PerformanceMetric +import com.datadog.android.core.metrics.TelemetryMetricType +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.android.lint.InternalApi +import com.datadog.tools.annotation.NoOpImplementation + +/** + * A Logger used to log messages from the internal implementation of the Datadog SDKs. + * + * Rule of thumb to decide which level and target we're using for the Internal Logger usage: + * + * - Target.USER: the message needs to either be actionable or provide information about the main + * steps in data processing (tracking, storage, upload). + * - Level.ERROR: for any actionable error originated from a user's configuration, preventing + * a feature from working, or for an issue resulting in unexpected data loss; + * - Level.WARN: to inform of an actionable misconfiguration or missuses of the SDK, resulting + * in delayed or incomplete data; + * - Level.INFO: information about important expected event (e.g.: successful upload); + * - Target.TELEMETRY: any event that need to be tracked for usage monitoring or for error + * diagnostic. + * - Level.ERROR, Level.WARN: for any critical error that is unexpected enough and actionable; + * - Level.INFO, Level.DEBUG, Level.VERBOSE: important information about critical parts of the + * SDK we want to monitor; + * - Target.MAINTAINER: can be anything relevant about the moving parts of the core SDK or any + * of the feature. Level is left to the discretion of the authors of a log. + * - Level.ERROR: for any caught error or situation preventing the SDK from working as expected; + * - Level.WARN: for any unexpected situation (e.g.: when one would use an IllegalStateException); + * - Level.INFO: information about internal high level steps of the SDK core or features; + * - Level.DEBUG: information about internal low level steps of the SDK core or features; + * - Level.VERBOSE: information on currently debugged feature or open ticket; + * + */ +@NoOpImplementation +interface InternalLogger { + + /** + * The severity level of a logged message. + */ + enum class Level { + /** + * Verbose level. + */ + VERBOSE, + + /** + * Debug level. + */ + DEBUG, + + /** + * Info level. + */ + INFO, + + /** + * Warning level. + */ + WARN, + + /** + * Error level. + */ + ERROR + } + + /** + * The target handler for a log message. + */ + enum class Target { + /** + * Log message will be sent to Logcat. + */ + USER, + + /** + * Log message will be sent to Logcat, but only in debug SDK builds. + */ + MAINTAINER, + + /** + * Log message will be sent to telemetry. + */ + TELEMETRY + } + + /** + * Logs a message from the internal implementation. + * @param level the severity level of the log + * @param target the target handler for the log + * @param messageBuilder the lambda building the log message + * @param throwable an optional throwable error + * @param onlyOnce whether only one instance of the message should be sent per lifetime of the + * logger (default is `false`) + * @param additionalProperties additional properties to add to the log + */ + fun log( + level: Level, + target: Target, + messageBuilder: () -> String, + throwable: Throwable? = null, + onlyOnce: Boolean = false, + additionalProperties: Map? = null + ) + + /** + * Logs a message from the internal implementation. + * @param level the severity level of the log + * @param targets list of the target handlers for the log + * @param messageBuilder the lambda building the log message + * @param throwable an optional throwable error + * @param onlyOnce whether only one instance of the message should be sent per lifetime of the + * logger (default is `false`, onlyOnce applies to each target independently) + * @param additionalProperties additional properties to add to the log + */ + fun log( + level: Level, + targets: List, + messageBuilder: () -> String, + throwable: Throwable? = null, + onlyOnce: Boolean = false, + additionalProperties: Map? = null + ) + + /** + * Logs a specific metric from the internal implementation. The metric values will be sent + * as key-value pairs in the additionalProperties and as part of the + * [com.datadog.android.telemetry.model.TelemetryDebugEvent.Telemetry] event. + * @param messageBuilder the lambda building the metric message + * @param additionalProperties additional properties to add to the metric + * @param samplingRate value between 0-100 for sampling the event. Note that the sampling rate applied to this + * @param creationSampleRate value between 0-100. Some of the metrics like [PerformanceMetric] being sampled on the + * metric creation place and then reported with 100% probability. In such cases we need to use *creationSampleRate* + * to compute effectiveSampleRate correctly. It's null by default means that metric sampled only when it + * reported which is applicable for most cases. creationSampleRate == null could + * be considered as creationSampleRate == 100% + */ + @InternalApi + fun logMetric( + messageBuilder: () -> String, + additionalProperties: Map, + @FloatRange(from = 0.0, to = 100.0) samplingRate: Float, + @FloatRange(from = 0.0, to = 100.0) creationSampleRate: Float? = null + ) + + /** + * Start measuring a performance metric. + * + * @param callerClass name of the class calling the performance measurement. + * @param metric name of the metric that we want to measure. + * @param samplingRate value between 0-100 for sampling the event. + * @param operationName the name of the operation being measured + * @return a PerformanceMetric object that can later be used to send telemetry, or null if sampled out + */ + @InternalApi + fun startPerformanceMeasure( + callerClass: String, + metric: TelemetryMetricType, + @FloatRange(from = 0.0, to = 100.0) samplingRate: Float, + operationName: String + ): PerformanceMetric? + + /** + * Logs an API usage from the internal implementation. + * @param samplingRate value between 0-100 for sampling the event. Note that the sampling rate applied to this + * event will be applied in addition to the global telemetry sampling rate. By default, the sampling rate is 15%. + * @param apiUsageEventBuilder the lambda building the API event being tracked + */ + @InternalApi + fun logApiUsage( + @FloatRange(from = 0.0, to = 100.0) samplingRate: Float = DEFAULT_API_USAGE_TELEMETRY_SAMPLING_RATE, + apiUsageEventBuilder: () -> InternalTelemetryEvent.ApiUsage + ) + + companion object { + + private const val DEFAULT_API_USAGE_TELEMETRY_SAMPLING_RATE = 15f + + /** + * Logger for the cases when SDK instance is not yet available. Try to use the logger + * provided by [FeatureSdkCore.internalLogger] instead if possible. + */ + val UNBOUND: InternalLogger = SdkInternalLogger(null) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/SdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/SdkCore.kt new file mode 100644 index 0000000000..5ad1b6aebc --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/SdkCore.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api + +import androidx.annotation.AnyThread +import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.privacy.TrackingConsent + +/** + * SdkCore is the entry point to register Datadog features to the core registry. + */ +@Suppress("ComplexInterface", "TooManyFunctions") +interface SdkCore { + + /** + * Name of the current SDK instance. + */ + val name: String + + /** + * The current time (both device and server). + */ + val time: TimeInfo + + /** + * Name of the service (given during the SDK initialization, otherwise package name is used). + */ + val service: String + + /** + * Returns true if the core is active. + */ + @AnyThread + fun isCoreActive(): Boolean + + /** + * Sets the tracking consent regarding the data collection for this instance of the Datadog SDK. + * + * @param consent which can take one of the values + * ([TrackingConsent.PENDING], [TrackingConsent.GRANTED], [TrackingConsent.NOT_GRANTED]) + */ + @AnyThread + fun setTrackingConsent(consent: TrackingConsent) + + /** + * Sets the user information. + * + * @param id a unique user identifier (relevant to your business domain) + * @param name (nullable) the user name or alias + * @param email (nullable) the user email + * @param extraInfo additional information. An extra information can be + * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. + */ + @AnyThread + fun setUserInfo( + id: String, + name: String? = null, + email: String? = null, + extraInfo: Map = emptyMap() + ) + + /** + * Sets additional information on the [UserInfo] object + * + * If properties had originally been set with [SdkCore.setUserInfo], they will be preserved. + * In the event of a conflict on key, the new property will prevail. + * + * @param extraInfo additional information. An extra information can be + * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. + */ + @AnyThread + fun addUserProperties(extraInfo: Map) + + /** + * Clear the current user information. + * + * User information will be set to null. + * After calling this api, Logs, Traces, RUM Events will not include the user information anymore. + * + * Any active RUM Session, active RUM View at the time of call will have their `usr` attribute cleared. + * + * If you want to retain the current `usr` on the active RUM session, + * you need to stop the session first by using `GlobalRumMonitor.get().stopSession()` + * + * If you want to retain the current `usr` on the active RUM views, + * you need to stop the view first by using `GlobalRumMonitor.get().stopView()` + */ + @AnyThread + fun clearUserInfo() + + /** + * Clears all unsent data in all registered features. + */ + @AnyThread + fun clearAllData() + + /** + * Sets the account information that the user is currently logged into. + * + * This API should be used to assign an identifier for the user's account which represents a + * contextual identity within the app, typically tied to business or tenant logic. The + * information set here will be added to logs, traces and RUM events. + * + * This value should be set when user logs in with his account, and cleared by calling + * [clearAccountInfo] when he logs out. + * + * @param id Account ID. + * @param name representing the account, if exists. + * @param extraInfo Account custom attributes, if exists. + */ + fun setAccountInfo( + id: String, + name: String? = null, + extraInfo: Map = emptyMap() + ) + + /** + * Add custom attributes to the current account information. + * + * This extra info will be added to already existing extra info that is added + * to Logs, Traces and RUM events automatically. + * + * @param extraInfo Account additional custom attributes. + */ + fun addAccountExtraInfo( + extraInfo: Map + ) + + /** + * Clear the current account information. + * + * Account information will be set to null. + * Following Logs, Traces, RUM Events will not include the account information anymore. + * + * Any active RUM Session, active RUM View at the time of call will have their `account` attribute cleared. + * + * If you want to retain the current `account` on the active RUM session, + * you need to stop the session first by using `GlobalRumMonitor.get().stopSession()`. + * + * If you want to retain the current `account` on the active RUM views, + * you need to stop the view first by using `GlobalRumMonitor.get().stopView()`. + * + */ + fun clearAccountInfo() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/AccountInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/AccountInfo.kt new file mode 100644 index 0000000000..01540146eb --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/AccountInfo.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +/** + * Holds information about the current account. + * @property id a unique identifier for the account, or null. + * @property name the name of the account, or null. + * @property extraInfo a dictionary of extra information to the current account. + */ +data class AccountInfo( + val id: String, + val name: String? = null, + val extraInfo: Map = emptyMap() +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DatadogContext.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DatadogContext.kt new file mode 100644 index 0000000000..bfce04fea5 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DatadogContext.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +import com.datadog.android.DatadogSite +import com.datadog.android.privacy.TrackingConsent + +/** + * Contains system information, as well as user-specific and feature specific context info. + * @property site [Datadog Site](https://docs.datadoghq.com/getting_started/site/) for data uploads. + * @property clientToken the client token allowing for data uploads to + * [Datadog Site](https://docs.datadoghq.com/getting_started/site/). + * @property service 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). + * @property env 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). + * @property version 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). + * @property variant the name of the application variant (if applies). + * @property source 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). + * @property sdkVersion the version of SDK. + * @property time the current time (both device and server) + * @property processInfo information about the current process + * @property networkInfo information about the current network availability and quality + * @property deviceInfo information about device + * @property userInfo information about the current user + * @property accountInfo information about the current account + * @property trackingConsent information about the current tracking consent + * @property appBuildId unique build ID of the running application. Will be missing if Datadog Gradle Plugin is not applied or obfuscation is not enabled for the running build. + * @property featuresContext agnostic dictionary with information from all features registered to + * the parent SDK instance + */ +data class DatadogContext( + val site: DatadogSite, + val clientToken: String, + val service: String, + val env: String, + val version: String, + val variant: String, + val source: String, + val sdkVersion: String, + val time: TimeInfo, + val processInfo: ProcessInfo, + val networkInfo: NetworkInfo, + val deviceInfo: DeviceInfo, + val userInfo: UserInfo, + val accountInfo: AccountInfo?, + val trackingConsent: TrackingConsent, + val appBuildId: String?, + val featuresContext: Map> +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DeviceInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DeviceInfo.kt new file mode 100644 index 0000000000..91bb055895 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DeviceInfo.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +/** + * Provides information about device and OS. + * + * @property deviceName Device marketing name, e.g. Samsung SM-988GN. + * @property deviceBrand Device marketing brand, e.g. Samsung. + * @property deviceModel Device SKU model, e.g. SM-988GN. + * @property deviceType Device type info. + * @property deviceBuildId Build Id, ex. "ac45fd". + * @property osName Operating system name, e.g. Android. + * @property osMajorVersion Major operating system version, e.g. 8. + * @property osVersion Full operating system version, e.g. 8.1.1. + * @property architecture The CPU architecture of the device. + * @property numberOfDisplays The number of displays on the device. + * @property localeInfo locale information on the device such as timezone and region settings. + */ +data class DeviceInfo( + val deviceName: String, + val deviceBrand: String, + val deviceModel: String, + val deviceType: DeviceType, + val deviceBuildId: String, + val osName: String, + val osMajorVersion: String, + val osVersion: String, + val architecture: String, + val numberOfDisplays: Int?, + val localeInfo: LocaleInfo +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DeviceType.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DeviceType.kt new file mode 100644 index 0000000000..5aef37d84d --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/DeviceType.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +/** + * Device type. + */ +enum class DeviceType { + /** + * Mobile device type. + */ + MOBILE, + + /** + * Tablet device type. + */ + TABLET, + + /** + * TV device type. + */ + TV, + + /** + * Desktop device type. + */ + DESKTOP, + + /** + * Gaming console device type. + */ + GAMING_CONSOLE, + + /** + * Bot type. + */ + BOT, + + /** + * Other device type. + */ + OTHER +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/LocaleInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/LocaleInfo.kt new file mode 100644 index 0000000000..21ab12d433 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/LocaleInfo.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +/** + * Provides information about locale. + * + * @property locales Ordered list of the user’s preferred system languages as IETF language tags. + * @property currentLocale The user's current locale as a language tag (language + region), computed from their preferences and the app's supported languages, e.g. 'es-FR'. + * @property timeZone The device’s current time zone identifier, e.g. 'Europe/Berlin'. + */ +data class LocaleInfo( + val locales: List, + val currentLocale: String, + val timeZone: String +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/NetworkInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/NetworkInfo.kt new file mode 100644 index 0000000000..5ec9f83556 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/NetworkInfo.kt @@ -0,0 +1,210 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Long +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +/** + * Holds information about the current network state. + * + * @property connectivity the current connectivity + * @property carrierName information about the network carrier, or null + * @property carrierId network carrier ID, or null + * @property upKbps the upload speed in kilobytes per second + * @property downKbps the download speed in kilobytes per second + * @property strength the strength of the signal (the unit depends on the type of the signal) + * @property cellularTechnology the type of cellular technology if known (e.g.: GPRS, LTE, 5G) + */ +data class NetworkInfo( + val connectivity: Connectivity = Connectivity.NETWORK_NOT_CONNECTED, + val carrierName: String? = null, + val carrierId: Long? = null, + val upKbps: Long? = null, + val downKbps: Long? = null, + val strength: Long? = null, + val cellularTechnology: String? = null +) { + internal fun toJson(): JsonElement { + val json = JsonObject() + json.add("connectivity", connectivity.toJson()) + carrierName?.let { carrierNameNonNull -> + json.addProperty("carrier_name", carrierNameNonNull) + } + carrierId?.let { carrierIdNonNull -> + json.addProperty("carrier_id", carrierIdNonNull) + } + upKbps?.let { upKbpsNonNull -> + json.addProperty("up_kbps", upKbpsNonNull) + } + downKbps?.let { downKbpsNonNull -> + json.addProperty("down_kbps", downKbpsNonNull) + } + strength?.let { strengthNonNull -> + json.addProperty("strength", strengthNonNull) + } + cellularTechnology?.let { cellularTechnologyNonNull -> + json.addProperty("cellular_technology", cellularTechnologyNonNull) + } + return json + } + + internal companion object { + @JvmStatic + @Throws(JsonParseException::class) + @Suppress("StringLiteralDuplication") + fun fromJson(jsonString: String): NetworkInfo { + try { + // JsonParseException is declared in the method signature + @Suppress("UnsafeThirdPartyFunctionCall") + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type NetworkInfo", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + @Suppress("StringLiteralDuplication", "ThrowsCount") + fun fromJsonObject(jsonObject: JsonObject): NetworkInfo { + try { + val connectivity = Connectivity.fromJson(jsonObject.get("connectivity").asString) + val carrierName = jsonObject.get("carrier_name")?.asString + val carrierId = jsonObject.get("carrier_id")?.asLong + val upKbps = jsonObject.get("up_kbps")?.asLong + val downKbps = jsonObject.get("down_kbps")?.asLong + val strength = jsonObject.get("strength")?.asLong + val cellularTechnology = jsonObject.get("cellular_technology")?.asString + return NetworkInfo( + connectivity, + carrierName, + carrierId, + upKbps, + downKbps, + strength, + cellularTechnology + ) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type NetworkInfo", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type NetworkInfo", + e + ) + } catch (@Suppress("TooGenericExceptionCaught") e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type NetworkInfo", + e + ) + } + } + } + + /** + * The type of connectivity currently available. + */ + enum class Connectivity( + private val jsonValue: String + ) { + /** + * The network is not connected. + */ + NETWORK_NOT_CONNECTED("network_not_connected"), + + /** + * The network is connected using a Ethernet connection. + */ + NETWORK_ETHERNET("network_ethernet"), + + /** + * The network is connected using a WiFi connection. + */ + NETWORK_WIFI("network_wifi"), + + /** + * The network is connected using a WiMax connection. + */ + NETWORK_WIMAX("network_wimax"), + + /** + * The network is connected using a Bluetooth connection. + */ + NETWORK_BLUETOOTH("network_bluetooth"), + + /** + * The network is connected using a 2G connection. + */ + NETWORK_2G("network_2G"), + + /** + * The network is connected using a 3G connection. + */ + NETWORK_3G("network_3G"), + + /** + * The network is connected using a 4G connection. + */ + NETWORK_4G("network_4G"), + + /** + * The network is connected using a 5G connection. + */ + NETWORK_5G("network_5G"), + + /** + * The network is connected using a cellular connection with a unknown technology. + */ + NETWORK_MOBILE_OTHER("network_mobile_other"), + + /** + * The network is connected using a cellular connection. + */ + NETWORK_CELLULAR("network_cellular"), + + /** + * The network is connected using an other connection type. + */ + NETWORK_OTHER("network_other") + ; + + internal fun toJson(): JsonElement = JsonPrimitive(jsonValue) + + internal companion object { + @JvmStatic + @Throws(JsonParseException::class) + fun fromJson(jsonString: String): Connectivity { + try { + return values().first { + it.jsonValue == jsonString + } + } catch (e: NoSuchElementException) { + throw JsonParseException( + "Unable to parse json into type NetworkInfo.Connectivity", + e + ) + } + } + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/ProcessInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/ProcessInfo.kt new file mode 100644 index 0000000000..c628f1ded7 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/ProcessInfo.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +/** + * Holds information about the current process. + * @property isMainProcess whether this is the main or a secondary process for the app + */ +data class ProcessInfo(val isMainProcess: Boolean) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/TimeInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/TimeInfo.kt new file mode 100644 index 0000000000..7728d46b33 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/TimeInfo.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +/** + * Holds information about the current local and server time. + * @property deviceTimeNs the current time as known by the System on the device (nanoseconds) + * @property serverTimeNs the current time synchronized with Datadog's NTP server(s) (nanoseconds) + * @property serverTimeOffsetNs the difference between server time and device time, relative + * to the device time (nanoseconds) + * @property serverTimeOffsetMs the difference between server time and device time, relative + * to the device time (milliseconds) + */ +data class TimeInfo( + val deviceTimeNs: Long, + val serverTimeNs: Long, + val serverTimeOffsetNs: Long, + val serverTimeOffsetMs: Long +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/UserInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/UserInfo.kt new file mode 100644 index 0000000000..b540e34c61 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/UserInfo.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +import com.datadog.android.core.internal.utils.JsonSerializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.lang.NumberFormatException +import kotlin.Any +import kotlin.Array +import kotlin.String +import kotlin.jvm.JvmStatic +import kotlin.jvm.Throws + +/** + * Holds information about the current User. + * @property anonymousId a unique anonymous identifier for the device, or null + * @property id a unique identifier for the user, or null + * @property name the name of the user, or null + * @property email the email address of the user, or null + * @property additionalProperties a dictionary of custom properties attached to the current user + */ +data class UserInfo( + val anonymousId: String? = null, + val id: String? = null, + val name: String? = null, + val email: String? = null, + val additionalProperties: Map = emptyMap() +) { + + @Suppress("StringLiteralDuplication") + internal fun toJson(): JsonElement { + val json = JsonObject() + anonymousId?.let { idNonNull -> + json.addProperty("anonymous_id", idNonNull) + } + id?.let { idNonNull -> + json.addProperty("id", idNonNull) + } + name?.let { nameNonNull -> + json.addProperty("name", nameNonNull) + } + email?.let { emailNonNull -> + json.addProperty("email", emailNonNull) + } + additionalProperties.forEach { (k, v) -> + if (k !in RESERVED_PROPERTIES) { + json.add(k, JsonSerializer.toJsonElement(v)) + } + } + return json + } + + internal companion object { + internal val RESERVED_PROPERTIES: Array = arrayOf("id", "name", "email") + + @JvmStatic + @Throws(JsonParseException::class) + @Suppress("StringLiteralDuplication") + fun fromJson(jsonString: String): UserInfo { + try { + // JsonParseException is declared in the method signature + @Suppress("UnsafeThirdPartyFunctionCall") + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return fromJsonObject(jsonObject) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type UserInfo", + e + ) + } + } + + @JvmStatic + @Throws(JsonParseException::class) + @Suppress("StringLiteralDuplication", "ThrowsCount") + fun fromJsonObject(jsonObject: JsonObject): UserInfo { + try { + val anonymousId = jsonObject.get("anonymous_id")?.asString + val id = jsonObject.get("id")?.asString + val name = jsonObject.get("name")?.asString + val email = jsonObject.get("email")?.asString + val additionalProperties = mutableMapOf() + for (entry in jsonObject.entrySet()) { + if (entry.key !in RESERVED_PROPERTIES) { + additionalProperties[entry.key] = entry.value + } + } + return UserInfo(anonymousId, id, name, email, additionalProperties) + } catch (e: IllegalStateException) { + throw JsonParseException( + "Unable to parse json into type UserInfo", + e + ) + } catch (e: NumberFormatException) { + throw JsonParseException( + "Unable to parse json into type UserInfo", + e + ) + } catch (@Suppress("TooGenericExceptionCaught") e: NullPointerException) { + throw JsonParseException( + "Unable to parse json into type UserInfo", + e + ) + } + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt new file mode 100644 index 0000000000..1747e4f213 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import android.content.Context + +/** + * Interface to be implemented by the feature, which doesn't require any storage, to be + * registered with [SdkCore]. + */ +interface Feature { + /** + * Name of the feature. + */ + val name: String + + /** + * This method is called during feature initialization. At this stage feature should setup itself. + * + * @param appContext Application context. + */ + fun onInitialize(appContext: Context) + + /** + * This method is called during feature de-initialization. At this stage feature should stop + * itself and release resources held. + */ + fun onStop() + + companion object { + // names of main features to have a single place where they are defined + + /** + * Logs feature name. + */ + const val LOGS_FEATURE_NAME: String = "logs" + + /** + * RUM feature name. + */ + const val RUM_FEATURE_NAME: String = "rum" + + /** + * Tracing feature name. + */ + const val TRACING_FEATURE_NAME: String = "tracing" + + /** + * Session Replay feature name. + */ + const val SESSION_REPLAY_FEATURE_NAME: String = "session-replay" + + /** + * Session Replay Resources sub-feature name. + */ + const val SESSION_REPLAY_RESOURCES_FEATURE_NAME: String = "session-replay-resources" + + /** + * NDK Crash Reports feature name. + */ + const val NDK_CRASH_REPORTS_FEATURE_NAME: String = "ndk-crash-reporting" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureContextUpdateReceiver.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureContextUpdateReceiver.kt new file mode 100644 index 0000000000..e283cab352 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureContextUpdateReceiver.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import androidx.annotation.AnyThread + +/** + * Receiver for feature context updates. + */ +fun interface FeatureContextUpdateReceiver { + + /** + * Called when the context for a feature is updated. + * @param featureName the name of the feature + * @param context the updated context + */ + @AnyThread + fun onContextUpdate(featureName: String, context: Map) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureEventReceiver.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureEventReceiver.kt new file mode 100644 index 0000000000..d91387df00 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureEventReceiver.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import androidx.annotation.AnyThread + +/** + * Interface to implement in order to receive events sent using [FeatureScope.sendEvent] API. + */ +fun interface FeatureEventReceiver { + + /** + * Method invoked when event is received. It will be invoked on the thread which sent event. + * + * @param event Incoming event. + */ + @AnyThread + fun onReceive(event: Any) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt new file mode 100644 index 0000000000..9faa4253f9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt @@ -0,0 +1,86 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import androidx.annotation.AnyThread +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.lint.InternalApi + +/** + * Represents a Datadog feature. + */ +interface FeatureScope { + + /** + * Property to enable interaction with the data store. + */ + val dataStore: DataStoreHandler + + /** + * Utility to write an event, asynchronously. + * @param withFeatureContexts Feature contexts ([DatadogContext.featuresContext] property) to include + * in the [DatadogContext] provided. The value should be the feature names as declared by [Feature.name]. + * Default is empty, meaning that no feature contexts will be included. + * @param callback an operation called with an up-to-date [DatadogContext] + * and an [EventWriteScope]. Callback will be executed on a single context processing worker thread. Execution of + * [EventWriteScope] will be done on a worker thread from I/O pool. + * [DatadogContext] will have a state created at the moment this method is called. + */ + @AnyThread + fun withWriteContext( + withFeatureContexts: Set = emptySet(), + callback: (datadogContext: DatadogContext, write: EventWriteScope) -> Unit + ) + + /** + * Utility to read current [DatadogContext], asynchronously. + * @param withFeatureContexts Feature contexts ([DatadogContext.featuresContext] property) to include + * in the [DatadogContext] provided. The value should be the feature names as declared by [Feature.name]. + * Default is empty, meaning that no feature contexts will be included. + * @param callback an operation called with an up-to-date [DatadogContext]. + * [DatadogContext] will have a state created at the moment this method is called. + */ + @AnyThread + fun withContext( + withFeatureContexts: Set = emptySet(), + callback: (datadogContext: DatadogContext) -> Unit + ) + + // TODO RUM-9852 Implement better passthrough mechanism for the JVM crash scenario + /** + * Same as [withWriteContext] but will be executed in the blocking manner. + * + * @param withFeatureContexts Feature contexts ([DatadogContext.featuresContext] property) to include + * in the [DatadogContext] provided. The value should be the feature names as declared by [Feature.name]. + * Default is empty, meaning that no feature contexts will be included. + * + * **NOTE**: This API is for the internal use only and is not guaranteed to be stable. + */ + @AnyThread + @InternalApi + fun getWriteContextSync(withFeatureContexts: Set = emptySet()): Pair? + + /** + * Send event to a given feature. It will be sent in a synchronous way. + * + * @param event Event to send. + */ + fun sendEvent(event: Any) + + /** + * Returns the original feature. + */ + fun unwrap(): T +} + +/** + * Scope for the event write operation which is invoked on the worker thread from I/O pool, which is different + * from the context processing worker thread used for [FeatureScope.withWriteContext] callback invocation. + */ +typealias EventWriteScope = ((EventBatchWriter) -> Unit) -> Unit diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScopeExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScopeExt.kt new file mode 100644 index 0000000000..68165f74bd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScopeExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import androidx.annotation.AnyThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.core.internal.SdkFeature +import com.datadog.android.core.metrics.TelemetryMetricType +import com.datadog.android.lint.InternalApi +import java.util.concurrent.Future + +/** + * Measures the execution time for the given block and report it as a MethodCall telemetry metric. + * @param R the type of the result of the operation + * @param callerClass the class calling the measured method + * @param operationName the operationName to report in the metric + * @param samplingRate the sampling rate for the metric + * @param operation the operation to report + */ +@InternalApi +fun InternalLogger.measureMethodCallPerf( + callerClass: Class<*>, + operationName: String, + samplingRate: Float = 100f, + operation: () -> R +): R { + val metric = startPerformanceMeasure( + callerClass = callerClass.name, + metric = TelemetryMetricType.MethodCalled, + samplingRate = samplingRate, + operationName = operationName + ) + + val result = operation() + + val isSuccessful = (result != null) && ((result !is Collection<*>) || result.isNotEmpty()) + metric?.stopAndSend(isSuccessful) + + return result +} + +/** + * Utility to read current [DatadogContext], asynchronously. + * @param withFeatureContexts Feature contexts ([DatadogContext.featuresContext] property) to include + * in the [DatadogContext] provided. The value should be the feature names as declared by [Feature.name]. + * Default is empty, meaning that no feature contexts will be included. + * + * Returns future that will contain [DatadogContext] in the state that it has at the moment of call. + */ +@AnyThread +@InternalApi +fun FeatureScope.getContextFuture( + withFeatureContexts: Set = emptySet() +): Future? { + return when (this) { + is SdkFeature -> getContextFuture(withFeatureContexts) + else -> null + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureSdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureSdkCore.kt new file mode 100644 index 0000000000..000d67f014 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureSdkCore.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import java.util.UUID +import java.util.concurrent.ExecutorService +import java.util.concurrent.ScheduledExecutorService + +/** + * Extension of [SdkCore] containing the necessary methods for the features development. + * + * SDK core is always guaranteed to implement this interface. + */ +@Suppress("TooManyFunctions") +interface FeatureSdkCore : SdkCore { + + /** + * Logger for the internal SDK purposes. + */ + val internalLogger: InternalLogger + + /** + * Registers a feature to this instance of the Datadog SDK. + * + * @param feature the feature to be registered. + */ + fun registerFeature(feature: Feature) + + /** + * Retrieves a registered feature. + * + * @param featureName the name of the feature to retrieve + * @return the registered feature with the given name, or null + */ + fun getFeature(featureName: String): FeatureScope? + + /** + * Updates the context if exists with the new entries. If there is no context yet for the + * provided [featureName], a new one will be created. + * + * @param featureName Feature name. + * @param useContextThread Whenever update of the context should happen on the context processing thread or not. It + * should be true for most of the cases related to the event processing. Be careful when setting it to false, valid + * use-case can be like updating/reading feature context on the same (or already on the context) thread. + * Defaults to true. + * @param updateCallback Provides current feature context for the update. If there is no feature + * with the given name registered, callback won't be called. + */ + fun updateFeatureContext( + featureName: String, + useContextThread: Boolean = true, + updateCallback: (context: MutableMap) -> Unit + ) + + /** + * Retrieves the context for the particular feature. + * + * @param featureName Feature name. + * @param useContextThread Whenever context read should happen on the context processing thread or not. It + * should be true for most of the cases related to the event processing. Be careful when setting it to false, valid + * use-case can be like updating/reading feature context on the same (or already on the context) thread. + * Defaults to true. + * @return Context for the given feature or empty map if feature is not registered. + */ + fun getFeatureContext(featureName: String, useContextThread: Boolean = true): Map + + /** + * Sets event receiver for the given feature. + * + * @param featureName Feature name. + * @param receiver Event receiver. + */ + fun setEventReceiver(featureName: String, receiver: FeatureEventReceiver) + + /** + * Removes events receive for the given feature. + * + * @param featureName Feature name. + */ + fun removeEventReceiver(featureName: String) + + /** + * Sets feature context update listener. Once subscribed, current context will be emitted + * immdediately if it exists. + * + * @param listener Listener to remove. + */ + fun setContextUpdateReceiver(listener: FeatureContextUpdateReceiver) + + /** + * Removes feature context update listener. + * + * @param listener Listener to remove. + */ + fun removeContextUpdateReceiver(listener: FeatureContextUpdateReceiver) + + /** + * Returns a new single thread [ExecutorService], set up with backpressure and internal monitoring. + * + * @param executorContext Context to be used for logging and naming threads running on this executor. + */ + fun createSingleThreadExecutorService(executorContext: String): ExecutorService + + /** + * Returns a new [ScheduledExecutorService], set up with internal monitoring. + * It will use a default of one thread and can spawn at most as many thread as there are CPU cores. + * + * @param executorContext Context to be used for logging and naming threads running on this executor. + */ + fun createScheduledExecutorService(executorContext: String): ScheduledExecutorService + + /** + * Allows the given feature to set the anonymous ID for the SDK. + * + * @param anonymousId Anonymous ID to set. Can be null if feature is disabled. + */ + fun setAnonymousId(anonymousId: UUID?) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/StorageBackedFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/StorageBackedFeature.kt new file mode 100644 index 0000000000..a6be3ef4dd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/StorageBackedFeature.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import com.datadog.android.api.net.RequestFactory +import com.datadog.android.api.storage.FeatureStorageConfiguration + +/** + * Interface to be implemented by the feature, which requires storage, to be + * registered with [SdkCore]. + */ +interface StorageBackedFeature : Feature { + + /** + * Provides an instance of [RequestFactory] for the given feature. Will be + * called before [Feature.onInitialize]. + */ + val requestFactory: RequestFactory + + /** + * Provides storage configuration for the given feature. Will be + * called before [Feature.onInitialize]. + */ + val storageConfiguration: FeatureStorageConfiguration +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/Request.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/Request.kt new file mode 100644 index 0000000000..3f067fd759 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/Request.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.net + +/** + * Request object holding the data to be sent. + * + * @property id Unique identifier of the request. + * @property description Description of the request (ex. "RUM request", "Logs request", etc.). + * @property url URL to call. + * @property headers Request headers. Note that User Agent header will be ignored. + * @property body Request payload. + * @property contentType Content type of the request, if needed. + */ +data class Request( + val id: String, + val description: String, + val url: String, + val headers: Map, + // won't generate custom equals/hashcode, because ID field is enough to identify the request + // and we don't want to have array content comparison + @Suppress("ArrayInDataClass") val body: ByteArray, + val contentType: String? = null +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/RequestExecutionContext.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/RequestExecutionContext.kt new file mode 100644 index 0000000000..ebd2cb66ee --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/RequestExecutionContext.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.net + +/** + * Provides information about the request execution context such as the number of attempts made to + * execute the request in case of a retry or the response code of the previous request failure code. + * @param attemptNumber the number of this attempt for a specific batch. + * It'll be 1 for the first attempt, and will be incremented each time an upload for the same batch is retried. + * This takes into account the initial request and all the retries. + * @param previousResponseCode the response code of the previous request failure code in case of a retry. + * In case of the initial request, this value will be null. + */ +data class RequestExecutionContext( + val attemptNumber: Int = 0, + val previousResponseCode: Int? = null +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/RequestFactory.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/RequestFactory.kt new file mode 100644 index 0000000000..2d93fd5d52 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/net/RequestFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.net + +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.RawBatchEvent + +/** + * Factory used to build requests from the batches stored. + */ +fun interface RequestFactory { + + /** + * Creates a request for the given batch. + * @param context Datadog SDK context. + * @param executionContext Information about the execution context this request in case of a previous retry. + * This information is specific to a certain batch and will be reset for the next batch in case of a drop or + * a successful request. + * @param batchData Raw data of the batch. + * @param batchMetadata Raw metadata of the batch. + * @throws [Exception] in case the request could not be created. + */ + fun create( + context: DatadogContext, + executionContext: RequestExecutionContext, + batchData: List, + batchMetadata: ByteArray? + ): Request? + + companion object { + /** + * application/json content type. + */ + const val CONTENT_TYPE_JSON: String = "application/json" + + /** + * text/plain;charset=UTF-8 content type. + */ + const val CONTENT_TYPE_TEXT_UTF8: String = "text/plain;charset=UTF-8" + + /** + * Datadog API key header. + */ + const val HEADER_API_KEY: String = "DD-API-KEY" + + /** + * Datadog Event Platform Origin header, e.g. android, flutter, etc. + */ + const val HEADER_EVP_ORIGIN: String = "DD-EVP-ORIGIN" + + /** + * Datadog Event Platform Origin version header, e.g. SDK version. + */ + const val HEADER_EVP_ORIGIN_VERSION: String = "DD-EVP-ORIGIN-VERSION" + + /** + * Datadog Request ID header, used for debugging purposes. + */ + const val HEADER_REQUEST_ID: String = "DD-REQUEST-ID" + + /** + * Datadog source query parameter name. + */ + const val QUERY_PARAM_SOURCE: String = "ddsource" + + /** + * Datadog tags query parameter name. + */ + const val QUERY_PARAM_TAGS: String = "ddtags" + + /** + * Datadog Idempotency key header, used to offer more insight into the request retry statistics. + */ + const val DD_IDEMPOTENCY_KEY: String = "DD-IDEMPOTENCY-KEY" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/DataWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/DataWriter.kt new file mode 100644 index 0000000000..90b84da760 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/DataWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage + +import androidx.annotation.WorkerThread +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Interface to be implemented by the class which wants to write arbitrary data with the + * given [EventBatchWriter]. + */ +@NoOpImplementation(publicNoOpImplementation = true) +interface DataWriter { + /** + * Writes the element with a given [EventBatchWriter]. + * + * @param writer the writer to use + * @param element the event to write + * @param eventType additional info about the event + * + * @return true if element was written, false otherwise. + */ + @WorkerThread + fun write(writer: EventBatchWriter, element: T, eventType: EventType): Boolean +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/EventBatchWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/EventBatchWriter.kt new file mode 100644 index 0000000000..f6ad848130 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/EventBatchWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage + +import androidx.annotation.WorkerThread + +/** + * Writer allowing [FeatureScope] to write events in the storage exposing current batch metadata. + */ +interface EventBatchWriter { + + /** + * @return the metadata of the current writeable batch + */ + @WorkerThread + fun currentMetadata(): ByteArray? + + /** + * Writes the content of the event to the current available batch. + * @param event the event to write (content + metadata) + * @param batchMetadata the optional updated batch metadata + * @param eventType additional information about the event data + * + * @return true if event was written, false otherwise. + */ + @WorkerThread + fun write( + event: RawBatchEvent, + batchMetadata: ByteArray?, + eventType: EventType + ): Boolean +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/EventType.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/EventType.kt new file mode 100644 index 0000000000..79850b9fa4 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/EventType.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage + +/** + * The type of event being sent to storage. + */ +enum class EventType { + /** A generic customer event (e.g.: log, span, …). */ + DEFAULT, + + /** A customer event related to a crash. */ + CRASH, + + /** An internal telemetry event to monitor the SDK's behavior and performances. */ + TELEMETRY +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/FeatureStorageConfiguration.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/FeatureStorageConfiguration.kt new file mode 100644 index 0000000000..3f38f4234c --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/FeatureStorageConfiguration.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage + +/** + * Contains the storage configuration for an [FeatureScope] instance. + * @property maxItemSize the maximum size (in bytes) for a single item in a batch + * @property maxItemsPerBatch the maximum number of individual items in a batch + * @property maxBatchSize the maximum size (in bytes) of a complete batch + * @property oldBatchThreshold the duration (in milliseconds) after which a batch is considered too + * old to be uploaded (usually because it'll be discarded at ingestion by the backend) + */ +data class FeatureStorageConfiguration( + val maxItemSize: Long, + val maxItemsPerBatch: Int, + val maxBatchSize: Long, + val oldBatchThreshold: Long +) { + companion object { + + /** + * Default storage configuration with the following parameters: + * max item size = 512 KB, + * max items per batch = 500, + * max batch size = 4 MB, + * old batch threshold = 18 hours. + */ + val DEFAULT: FeatureStorageConfiguration = FeatureStorageConfiguration( + // 512 KB + maxItemSize = 512L * 1024, + maxItemsPerBatch = 500, + // 4 MB + maxBatchSize = 4L * 1024 * 1024, + // 18 hours + oldBatchThreshold = 18L * 60L * 60L * 1000L + ) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/RawBatchEvent.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/RawBatchEvent.kt new file mode 100644 index 0000000000..c0d6b8d95f --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/RawBatchEvent.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage + +/** + * Representation of the raw data which is going to be written in the batch file. + * + * @property data Raw data to write. + * @property metadata Optional metadata to write for this event. + */ +data class RawBatchEvent( + val data: ByteArray, + val metadata: ByteArray = EMPTY_BYTE_ARRAY +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawBatchEvent + + if (!data.contentEquals(other.data)) return false + if (!metadata.contentEquals(other.metadata)) return false + + return true + } + + override fun hashCode(): Int { + var result = data.contentHashCode() + result = 31 * result + metadata.contentHashCode() + return result + } + + private companion object { + val EMPTY_BYTE_ARRAY = ByteArray(0) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt new file mode 100644 index 0000000000..9df40f36cc --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage.datastore + +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.persistence.Serializer + +/** + * Interface for the datastore. + */ +interface DataStoreHandler { + + /** + * Write data to the datastore. + * This executes on a worker thread and not on the caller thread. + * + * @param T datatype of the data to write to the datastore. + * @param key name of the datastore entry. + * @param data to write. + * @param version optional version for the entry. + * If not specified will give the entry version 0 - even if that would be a downgrade from the previous version. + * @param callback (optional) to indicate whether the operation succeeded or not. + * @param serializer to use to serialize the data. + */ + fun setValue( + key: String, + data: T, + version: Int = 0, + callback: DataStoreWriteCallback? = null, + serializer: Serializer + ) + + /** + * Read data from the datastore. + * This executes on a worker thread and not on the caller thread. + * + * @param T datatype of the data to read from the datastore. + * @param key name of the datastore entry. + * @param version optional version to use when reading from the datastore. + * If specified, will only return data if the persistent entry exactly matches this version number. + * @param callback to return result asynchronously. + * @param deserializer to use to deserialize the data. + */ + fun value( + key: String, + version: Int? = null, + callback: DataStoreReadCallback, + deserializer: Deserializer + ) + + /** + * Remove an entry from the datastore. + * This executes on a worker thread and not on the caller thread. + * + * @param key name of the datastore entry + * @param callback (optional) to indicate whether the operation succeeded or not. + */ + fun removeValue( + key: String, + callback: DataStoreWriteCallback? = null + ) + + /** + * Removes all saved datastore entries. + */ + fun clearAllData() + + companion object { + /** + * The current version of the datastore. + */ + const val CURRENT_DATASTORE_VERSION: Int = 0 + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreReadCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreReadCallback.kt new file mode 100644 index 0000000000..3af901890a --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreReadCallback.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage.datastore + +import com.datadog.android.core.persistence.datastore.DataStoreContent + +/** + * Callback for asynchronous read operations on the datastore. + * @param T the datatype being retrieved. + */ +interface DataStoreReadCallback { + + /** + * Triggered on successfully reading data from the datastore. + * + * @param dataStoreContent (nullable) contains the datastore content if there was data to fetch, else null. + */ + fun onSuccess(dataStoreContent: DataStoreContent?) + + /** + * Triggered on failing to read data from the datastore. + */ + fun onFailure() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreWriteCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreWriteCallback.kt new file mode 100644 index 0000000000..393ba9314c --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreWriteCallback.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.storage.datastore + +/** + * Callback for asynchronous write operations on the datastore. + */ +interface DataStoreWriteCallback { + /** + * Triggered on successfully writing data to the datastore. + */ + fun onSuccess() + + /** + * Triggered on failing to write data to the datastore. + */ + fun onFailure() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt new file mode 100644 index 0000000000..3924e20cbd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core + +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.lint.InternalApi +import com.datadog.android.privacy.TrackingConsent +import com.google.gson.JsonObject +import java.io.File +import java.util.concurrent.ExecutorService + +/** + * FOR INTERNAL USAGE ONLY. THIS INTERFACE CONTENT MAY CHANGE WITHOUT NOTICE. + */ +interface InternalSdkCore : FeatureSdkCore { + + /** + * Returns current state of network connection. + */ + @InternalApi + val networkInfo: NetworkInfo + + /** + * Current tracking consent. + */ + @InternalApi + val trackingConsent: TrackingConsent + + /** + * Root folder for the hosting SDK instance. + */ + @InternalApi + val rootStorageDir: File? + + /** + * Shows if core is running in developer mode (some settings are overwritten to simplify + * debugging during app development). + */ + @InternalApi + val isDeveloperModeEnabled: Boolean + + /** + * Returns an instance of [FirstPartyHostHeaderTypeResolver] associated with the current + * SDK instance. + */ + val firstPartyHostResolver: FirstPartyHostHeaderTypeResolver + + /** + * Reads last known RUM view event stored. + */ + @get:WorkerThread + @InternalApi + val lastViewEvent: JsonObject? + + /** + * Reads information about last fatal ANR sent. + */ + @get:WorkerThread + @InternalApi + val lastFatalAnrSent: Long? + + /** + * Provide the time the application started in nanoseconds from device boot, or our best guess + * if the actual start time is not available. Note: since the implementation may rely on [System.nanoTime], + * this property can only be used to measure elapsed time and is not related to any other notion of system + * or wall-clock time. The value is the time since VM start. + */ + @InternalApi + val appStartTimeNs: Long + + /** + * Writes current RUM view event to the dedicated file for the needs of NDK crash reporting. + * + * @param data Serialized RUM view event. + */ + @InternalApi + @WorkerThread + fun writeLastViewEvent(data: ByteArray) + + /** + * Deletes last RUM view event written. + */ + @InternalApi + @WorkerThread + fun deleteLastViewEvent() + + /** + * Writes timestamp of the last fatal ANR sent. + */ + @InternalApi + @WorkerThread + fun writeLastFatalAnrSent(anrTimestamp: Long) + + /** + * Get an executor service for persistence purposes. + * @return the persistence executor to use for this SDK + */ + @InternalApi + @AnyThread + fun getPersistenceExecutorService(): ExecutorService + + /** + * @return all the registered features. + */ + @InternalApi + fun getAllFeatures(): List + + /** + * @param withFeatureContexts Feature contexts ([DatadogContext.featuresContext] property) to include + * in the [DatadogContext] provided. The value should be the feature names as declared by [Feature.name]. + * Default is empty, meaning that no feature contexts will be included. + * @return the current [DatadogContext], or null + */ + @InternalApi + fun getDatadogContext(withFeatureContexts: Set = emptySet()): DatadogContext? +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/SdkReference.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/SdkReference.kt new file mode 100644 index 0000000000..df060bc722 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/SdkReference.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core + +import com.datadog.android.Datadog +import com.datadog.android.api.SdkCore +import com.datadog.android.core.internal.DatadogCore +import java.util.concurrent.atomic.AtomicReference + +/** + * Class establishing the reference to the particular SDK instance by using its name. + * + * Once SDK instance with given name is available (during the [SdkReference.get] call), it will be + * kept in this class and callback [onSdkInstanceCaptured] will be fired. + * + * Once SDK instance with given name becomes inactive (it is stopped), reference will be + * automatically cleaned up. + * + * @param sdkInstanceName Name of the SDK instance to capture. If no name is provided, default + * SDK instance will be checked. + * @param onSdkInstanceCaptured Callback which will be fired once SDK instance is acquired. + */ +class SdkReference +@JvmOverloads +constructor( + private val sdkInstanceName: String? = null, + private val onSdkInstanceCaptured: (SdkCore) -> Unit = {} +) { + + private val reference = AtomicReference(null) + + /** + * Returns SDK instance if it is acquired, null otherwise. + */ + fun get(): SdkCore? { + val current = reference.get() + return if (current == null) { + tryAcquire() + } else { + val isActive = (current as? DatadogCore)?.isActive + if (isActive != null && !isActive) { + reference.compareAndSet(current, null) + null + } else { + current + } + } + } + + private fun tryAcquire(): SdkCore? { + return synchronized(reference) { + val current = reference.get() + @Suppress("IfThenToElvis") // Less readable + if (current != null) { + current + } else if (Datadog.isInitialized(sdkInstanceName)) { + val sdkCore = Datadog.getInstance(sdkInstanceName) + reference.set(sdkCore) + onSdkInstanceCaptured(sdkCore) + sdkCore + } else { + null + } + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/UploadWorker.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/UploadWorker.kt new file mode 100644 index 0000000000..66b88a7916 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/UploadWorker.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core + +import android.content.Context +import androidx.annotation.WorkerThread +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.NoOpInternalSdkCore +import com.datadog.android.core.internal.SdkFeature +import com.datadog.android.core.internal.data.upload.DataUploader +import com.datadog.android.core.internal.data.upload.UploadStatus +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.persistence.BatchId +import com.datadog.android.core.internal.utils.unboundInternalLogger +import java.util.LinkedList +import java.util.Queue + +/** + * `UploadWorker` is responsible for handling background upload tasks using WorkManager. This + * worker is designed to process Datadog upload jobs asynchronously. + * + * ## Important: + * **This worker must be used only when a custom WorkFactory is implemented.** + * + * @constructor Creates an instance of `UploadWorker`. + * @param appContext The application context. + * @param workerParams Parameters required for the worker execution. + */ +class UploadWorker( + appContext: Context, + workerParams: WorkerParameters +) : Worker(appContext, workerParams) { + + // region Worker + + @WorkerThread + override fun doWork(): Result { + // the idea behind upload is the following: + // 1. we shuffle features list to randomize initial upload task sequence. It is done to + // avoid the possible bottleneck when some feature has big batches which are uploaded + // slowly, so that next time other features don't wait and have a chance to go before. + // 2. we introduce FIFO queue also to avoid the bottleneck: if some feature batch cannot + // be uploaded we put retry task to the end of queue, so that batches of other features + // have a chance to go. + val instanceName = inputData.getString(DATADOG_INSTANCE_NAME) + val sdkCore = Datadog.getInstance(instanceName) as? InternalSdkCore + if (sdkCore == null || sdkCore is NoOpInternalSdkCore) { + unboundInternalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { MESSAGE_NOT_INITIALIZED } + ) + return Result.success() + } + + val features = sdkCore.getAllFeatures().mapNotNull { it as? SdkFeature }.shuffled() + + val tasksQueue = LinkedList() + + features.forEach { + @Suppress("UnsafeThirdPartyFunctionCall") // safe to add + tasksQueue.offer(UploadNextBatchTask(tasksQueue, sdkCore, it)) + } + + while (!tasksQueue.isEmpty()) { + tasksQueue.poll()?.run() + } + + return Result.success() + } + + // endregion + + // region Internal + + internal class UploadNextBatchTask( + private val taskQueue: Queue, + private val sdkCore: InternalSdkCore, + private val feature: SdkFeature + ) : Runnable { + + @WorkerThread + override fun run() { + // context is unique for each batch query instead of using the same one for all the + // batches which will be uploaded, because it can change by the time the upload + // of the next batch is requested. + val context = sdkCore.getDatadogContext() ?: return + + val storage = feature.storage + val uploader = feature.uploader + val nextBatchData = storage.readNextBatch() + if (nextBatchData != null) { + val uploadStatus = consumeBatch( + nextBatchData.id, + context, + nextBatchData.data, + nextBatchData.metadata, + uploader + ) + storage.confirmBatchRead( + nextBatchData.id, + RemovalReason.IntakeCode(uploadStatus.code), + deleteBatch = !uploadStatus.shouldRetry + ) + if (uploadStatus is UploadStatus.Success) { + @Suppress("UnsafeThirdPartyFunctionCall") // safe to add + taskQueue.offer(UploadNextBatchTask(taskQueue, sdkCore, feature)) + } + } + } + + private fun consumeBatch( + batchId: BatchId, + context: DatadogContext, + batch: List, + batchMeta: ByteArray?, + uploader: DataUploader + ): UploadStatus { + return uploader.upload(context, batch, batchMeta, batchId) + } + } + + // endregion + + companion object { + + internal const val MESSAGE_NOT_INITIALIZED = "Datadog has not been initialized." + + internal const val DATADOG_INSTANCE_NAME = "_dd.sdk.instanceName" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BackPressureMitigation.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BackPressureMitigation.kt new file mode 100644 index 0000000000..61d2e9c7d1 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BackPressureMitigation.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +/** + * Defines the mitigation to use when a queue hits the maximum back pressure capacity. + */ +enum class BackPressureMitigation { + + /** Drop the oldest items already in the queue to make room for new ones. */ + DROP_OLDEST, + + /** Ignore newest items that are not yet in the queue. */ + IGNORE_NEWEST +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BackPressureStrategy.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BackPressureStrategy.kt new file mode 100644 index 0000000000..627ec795fc --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BackPressureStrategy.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +/** + * @param capacity the maximum size of the queue + * @param onThresholdReached callback called when the queue reaches full capacity + * @param onItemDropped called when an item is dropped because of this backpressure strategy + * @param backpressureMitigation the mitigation to use when reaching the capacity + */ +data class BackPressureStrategy( + val capacity: Int, + val onThresholdReached: () -> Unit, + val onItemDropped: (Any) -> Unit, + val backpressureMitigation: BackPressureMitigation +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BatchProcessingLevel.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BatchProcessingLevel.kt new file mode 100644 index 0000000000..d2e1f78fca --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BatchProcessingLevel.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +/** + * Defines the policy for sending the batches. + * High level will mean that more data will be sent in a single upload cycle but more CPU and memory + * will be used to process the data. + * Low level will mean that less data will be sent in a single upload cycle but less CPU and memory + * will be used to process the data. + * @param maxBatchesPerUploadJob the maximum number of batches that will be sent in a single upload + * cycle. + */ +@Suppress("MagicNumber") +enum class BatchProcessingLevel(val maxBatchesPerUploadJob: Int) { + /** + * Only 1 batch will be sent in a single upload cycle. + */ + LOW(maxBatchesPerUploadJob = 1), + + /** + * 20 batches will be sent in a single upload cycle. + */ + MEDIUM(maxBatchesPerUploadJob = 20), + + /** + * 100 batches will be sent in a single upload cycle. + */ + HIGH(maxBatchesPerUploadJob = 100) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BatchSize.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BatchSize.kt new file mode 100644 index 0000000000..76583f7a0f --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/BatchSize.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +/** + * Defines the policy when batching data together. + * Smaller batches will means smaller but more network requests, + * whereas larger batches will mean fewer but larger network requests. + */ +@Suppress("MagicNumber") +enum class BatchSize( + internal val windowDurationMs: Long +) { + + /** Prefer small batches. **/ + SMALL(3000L), + + /** Prefer medium sized batches. **/ + MEDIUM(10000L), + + /** Prefer large batches. **/ + LARGE(35000L) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt new file mode 100644 index 0000000000..d64a434c75 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt @@ -0,0 +1,317 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +import com.datadog.android.Datadog +import com.datadog.android.DatadogSite +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.security.Encryption +import com.datadog.android.trace.TracingHeaderType +import okhttp3.Authenticator +import java.net.Proxy + +/** + * An object describing the configuration of the Datadog SDK. + * + * This is necessary to initialize the SDK with the [Datadog.initialize] method. + */ +data class Configuration +internal constructor( + internal val coreConfig: Core, + internal val clientToken: String, + internal val env: String, + internal val variant: String, + internal val service: String?, + internal val crashReportsEnabled: Boolean, + internal val additionalConfig: Map +) { + + internal data class Core( + val needsClearTextHttp: Boolean, + val enableDeveloperModeWhenDebuggable: Boolean, + val firstPartyHostsWithHeaderTypes: Map>, + val batchSize: BatchSize, + val uploadFrequency: UploadFrequency, + val proxy: Proxy?, + val proxyAuth: Authenticator, + val encryption: Encryption?, + val site: DatadogSite, + val batchProcessingLevel: BatchProcessingLevel, + val persistenceStrategyFactory: PersistenceStrategy.Factory?, + val backpressureStrategy: BackPressureStrategy, + val uploadSchedulerStrategy: UploadSchedulerStrategy? + ) + + // region Builder + + /** + * A Builder class for a [Configuration]. + * + * @param clientToken your API key of type Client Token + * @param env the environment name that will be sent with each event. This can be used to + * filter your events on different environments (e.g.: "staging" vs. "production"). + * @param variant the variant of your application, which should be the value from your + * `BuildConfig.FLAVOR` constant if you have different flavors, empty string otherwise. + * @param service the service name (if set to null, it'll be set to your application's + * package name, e.g.: com.example.android) + */ + @Suppress("TooManyFunctions") + class Builder + @JvmOverloads + constructor( + private val clientToken: String, + private val env: String, + private val variant: String = NO_VARIANT, + private val service: String? = null + ) { + private var additionalConfig: Map = emptyMap() + + private var coreConfig = DEFAULT_CORE_CONFIG + private var crashReportsEnabled: Boolean = true + + internal var hostsSanitizer = HostsSanitizer() + + /** + * Builds a [Configuration] based on the current state of this Builder. + */ + fun build(): Configuration { + return Configuration( + coreConfig = coreConfig, + clientToken = clientToken, + env = env, + variant = variant, + service = service, + crashReportsEnabled = crashReportsEnabled, + additionalConfig = additionalConfig + ) + } + + /** + * Sets the DataDog SDK to be more verbose when an application is set to `debuggable`. + * This is equivalent to setting: + * setSessionSampleRate(100) + * setBatchSize(BatchSize.SMALL) + * setUploadFrequency(UploadFrequency.FREQUENT) + * Datadog.setVerbosity(Log.VERBOSE) + * These settings will override your configuration, but only when the application is `debuggable` + * @param developerModeEnabled Enable or disable extra debug info when an app is debuggable + */ + @Suppress("FunctionMaxLength") + fun setUseDeveloperModeWhenDebuggable(developerModeEnabled: Boolean): Builder { + coreConfig = coreConfig.copy(enableDeveloperModeWhenDebuggable = developerModeEnabled) + return this + } + + /** + * Sets the list of first party hosts. + * Requests made to a URL with any one of these hosts (or any subdomain) will: + * - be considered a first party resource and categorised as such in your RUM dashboard; + * - be wrapped in a Span and have DataDog trace id injected to get a full flame-graph in + * APM in case of OkHttp instrumentation usage. + * @param hosts a list of all the hosts that you own. + */ + fun setFirstPartyHosts(hosts: List): Builder { + val sanitizedHosts = hostsSanitizer.sanitizeHosts( + hosts, + NETWORK_REQUESTS_TRACKING_FEATURE_NAME + ) + coreConfig = coreConfig.copy( + firstPartyHostsWithHeaderTypes = sanitizedHosts.associateWith { + setOf( + TracingHeaderType.DATADOG, + TracingHeaderType.TRACECONTEXT + ) + } + ) + return this + } + + /** + * Sets the list of first party hosts and specifies the type of HTTP headers used for + * distributed tracing. + * Requests made to a URL with any one of these hosts (or any subdomain) will: + * - be considered a first party resource and categorised as such in your RUM dashboard; + * - be wrapped in a Span and have trace id of the specified types injected to get a + * full flame-graph in APM. Multiple header types are supported for each host. + * @param hostsWithHeaderType a list of all the hosts that you own and the tracing headers + * to be used for each host. + * See [DatadogInterceptor] + */ + fun setFirstPartyHostsWithHeaderType(hostsWithHeaderType: Map>): Builder { + val sanitizedHosts = hostsSanitizer.sanitizeHosts( + hostsWithHeaderType.keys.toList(), + NETWORK_REQUESTS_TRACKING_FEATURE_NAME + ) + coreConfig = coreConfig.copy( + firstPartyHostsWithHeaderTypes = hostsWithHeaderType.filterKeys { sanitizedHosts.contains(it) } + ) + return this + } + + /** + * Let the SDK target your preferred Datadog's site. + */ + fun useSite(site: DatadogSite): Builder { + coreConfig = coreConfig.copy(needsClearTextHttp = false, site = site) + return this + } + + /** + * Defines the batch size (impacts the size and number of requests performed by Datadog). + * @param batchSize the desired batch size + */ + fun setBatchSize(batchSize: BatchSize): Builder { + coreConfig = coreConfig.copy(batchSize = batchSize) + return this + } + + /** + * Defines the preferred upload frequency. + * @param uploadFrequency the desired upload frequency policy + */ + fun setUploadFrequency(uploadFrequency: UploadFrequency): Builder { + coreConfig = coreConfig.copy(uploadFrequency = uploadFrequency) + return this + } + + /** + * Defines the Batch processing level, defining the maximum number of batches processed + * sequentially without a delay within one reading/uploading cycle. + * @param batchProcessingLevel the desired batch processing level. By default it's set to + * [BatchProcessingLevel.MEDIUM]. + * @see BatchProcessingLevel + */ + fun setBatchProcessingLevel(batchProcessingLevel: BatchProcessingLevel): Builder { + coreConfig = coreConfig.copy(batchProcessingLevel = batchProcessingLevel) + return this + } + + /** + * Allows to provide additional configuration values which can be used by the SDK. + * @param additionalConfig Additional configuration values. + */ + fun setAdditionalConfiguration(additionalConfig: Map): Builder { + return apply { + this.additionalConfig = additionalConfig + } + } + + /** + * Enables a custom proxy for uploading tracked data to Datadog's intake. + * @param proxy the [Proxy] configuration + * @param authenticator the optional [Authenticator] for the proxy + */ + fun setProxy(proxy: Proxy, authenticator: Authenticator?): Builder { + coreConfig = coreConfig.copy( + proxy = proxy, + proxyAuth = authenticator ?: Authenticator.NONE + ) + return this + } + + /** + * Allows to set the encryption for the local data. By default no encryption is used for + * the local data. + * + * @param dataEncryption An encryption object complying [Encryption] interface. + */ + fun setEncryption(dataEncryption: Encryption): Builder { + coreConfig = coreConfig.copy( + encryption = dataEncryption + ) + return this + } + + /** + * Allows to use a custom persistence strategy. + * @param persistenceStrategyFactory the persistence strategy to use (or null to use the default one) + */ + fun setPersistenceStrategyFactory(persistenceStrategyFactory: PersistenceStrategy.Factory?): Builder { + coreConfig = coreConfig.copy( + persistenceStrategyFactory = persistenceStrategyFactory + ) + return this + } + + /** + * Allows to control if JVM crashes are tracked or not. Default value is `true`. + * + * @param crashReportsEnabled whether crashes are tracked and sent to Datadog + */ + fun setCrashReportsEnabled(crashReportsEnabled: Boolean): Builder { + this.crashReportsEnabled = crashReportsEnabled + return this + } + + /** + * Sets the strategy to handle scalability issues. + * Many operations (data processing, event I/O, …) are queued in background threads. + * This configuration lets one decide how to handle the edge case when the queue starts growing, which can lead + * to a lot of memory usage, delayed processing, and possibly OOM or ANR. + * @param backpressureStrategy the backpressure strategy (default strategy ignores new tasks if a queue reaches + * 1024 items) + */ + fun setBackpressureStrategy(backpressureStrategy: BackPressureStrategy): Builder { + coreConfig = coreConfig.copy(backpressureStrategy = backpressureStrategy) + return this + } + + /** + * Sets the strategy to schedule data uploads. + * @param uploadSchedulerStrategy the upload scheduler strategy, + * or null to use the default strategy (default: null) + */ + fun setUploadSchedulerStrategy(uploadSchedulerStrategy: UploadSchedulerStrategy?): Builder { + coreConfig = coreConfig.copy(uploadSchedulerStrategy = uploadSchedulerStrategy) + return this + } + + internal fun allowClearTextHttp(): Builder { + coreConfig = coreConfig.copy( + needsClearTextHttp = true + ) + return this + } + } + + // endregion + + companion object { + + /** + * Value to use if application doesn't have flavors. + */ + private const val NO_VARIANT: String = "" + + private const val DEFAULT_BACKPRESSURE_THRESHOLD = 1024 + + internal val DEFAULT_BACKPRESSURE_STRATEGY = BackPressureStrategy( + DEFAULT_BACKPRESSURE_THRESHOLD, + {}, + {}, + BackPressureMitigation.IGNORE_NEWEST + ) + + internal val DEFAULT_CORE_CONFIG = Core( + needsClearTextHttp = false, + enableDeveloperModeWhenDebuggable = false, + firstPartyHostsWithHeaderTypes = emptyMap(), + batchSize = BatchSize.MEDIUM, + uploadFrequency = UploadFrequency.AVERAGE, + proxy = null, + proxyAuth = Authenticator.NONE, + encryption = null, + site = DatadogSite.US1, + batchProcessingLevel = BatchProcessingLevel.MEDIUM, + persistenceStrategyFactory = null, + backpressureStrategy = DEFAULT_BACKPRESSURE_STRATEGY, + uploadSchedulerStrategy = null + ) + + internal const val NETWORK_REQUESTS_TRACKING_FEATURE_NAME = "Network requests" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/HostsSanitizer.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/HostsSanitizer.kt new file mode 100644 index 0000000000..48e06c380f --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/HostsSanitizer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.utils.unboundInternalLogger +import com.datadog.android.lint.InternalApi +import java.net.MalformedURLException +import java.net.URL +import java.util.Locale + +/** + * Utility class with the goal to perform host sanitization. Not intended for the public use. + */ +class HostsSanitizer { + + /** + * Performs hosts sanitization by comparing them with patterns from pre-defined set. + * + * @param hosts Hosts to sanitize. + * @param feature SDK feature requesting the sanitization. + */ + @InternalApi + fun sanitizeHosts( + hosts: List, + feature: String + ): List { + val validHostNameRegEx = Regex(VALID_HOSTNAME_REGEX) + val validUrlRegex = Regex(URL_REGEX) + return hosts.mapNotNull { + if (it.matches(validUrlRegex)) { + try { + val parsedUrl = URL(it) + unboundInternalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { + WARNING_USING_URL.format( + Locale.US, + it, + feature, + parsedUrl.host + ) + } + ) + parsedUrl.host + } catch (e: MalformedURLException) { + unboundInternalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_MALFORMED_URL.format(Locale.US, it, feature) }, + e + ) + null + } + } else if (it.matches(validHostNameRegEx)) { + it + } else if (it.lowercase(Locale.US) == "localhost") { + // special rule exception to accept `localhost` as a valid domain name + it + } else { + unboundInternalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_MALFORMED_HOST_IP_ADDRESS.format(Locale.US, it, feature) } + ) + null + } + } + } + + internal companion object { + private const val VALID_IP_REGEX: String = + "^(([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])$" + private const val VALID_DOMAIN_REGEX: String = + "^(([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 const val VALID_HOSTNAME_REGEX: String = "$VALID_IP_REGEX|$VALID_DOMAIN_REGEX" + private const val URL_REGEX: String = "^(http|https)://(.*)" + + internal const val WARNING_USING_URL: String = + "You are using a url \"%s\" instead of a host to setup %s tracking. " + + "You should use instead a valid host name: \"%s\"" + + internal const val ERROR_MALFORMED_URL: String = "You are using a malformed url \"%s\" " + + "to setup %s tracking. It will be dropped. " + + "Please try using a host name instead, e.g.: \"example.com\"" + + internal const val ERROR_MALFORMED_HOST_IP_ADDRESS: String = + "You are using a malformed host or ip address \"%s\" to setup %s tracking. " + + "It will be dropped." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/UploadFrequency.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/UploadFrequency.kt new file mode 100644 index 0000000000..2f518fde88 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/UploadFrequency.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +/** + * Defines the frequency at which batch upload are tried. + */ +@Suppress("MagicNumber") +enum class UploadFrequency( + internal val baseStepMs: Long +) { + + /** Try to upload batch data frequently. */ + FREQUENT(500L), + + /** Try to upload batch data with a medium frequency. */ + AVERAGE(2000L), + + /** Try to upload batch data rarely. */ + RARE(5000L) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/UploadSchedulerStrategy.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/UploadSchedulerStrategy.kt new file mode 100644 index 0000000000..c993595a83 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/UploadSchedulerStrategy.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +import com.datadog.android.api.feature.Feature + +/** + * Defines the strategy used to schedule the waiting period between batch uploads. + */ +interface UploadSchedulerStrategy { + + /** + * Should return the delay in milliseconds to wait until the next upload attempt + * is performed. + * @param featureName the name of the feature for which a new upload will be scheduled. Known feature names are + * listed in the [Feature.Companion] object. + * @param uploadAttempts the number of requests that were attempted during the last upload batch. Will be zero if + * the device is not ready (e.g.: when offline or with low battery) or no data is ready to be sent. + * If multiple batches can be uploaded, the attempts will stop at the first failure. + * @param lastStatusCode the HTTP status code of the last request (if available). A successful upload will have a + * status code 202 (Accepted). When null, it means that the network request didn't fully complete. + * @param throwable the exception thrown during the upload process (if any). + */ + fun getMsDelayUntilNextUpload( + featureName: String, + uploadAttempts: Int, + lastStatusCode: Int?, + throwable: Throwable? + ): Long +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/constraints/DataConstraints.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/constraints/DataConstraints.kt new file mode 100644 index 0000000000..0da8421ff2 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/constraints/DataConstraints.kt @@ -0,0 +1,44 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.constraints + +/** + * This interface allows sanitizing logs locally before uploading them to the servers. + */ +interface DataConstraints { + + /** + * Validates attributes according to the particular rules. + * + * @param T type of attribute values. + * @param attributes Attributes to validate. + * @param keyPrefix Optional key prefix for the attributes root (ex. "usr"). + * @param attributesGroupName Optional description for the attributes to validate. + * @param reservedKeys Collection of reserved key. If attribute name is in the reserved key, + * this attribute will be rejected. + */ + fun validateAttributes( + attributes: Map, + keyPrefix: String? = null, + attributesGroupName: String? = null, + reservedKeys: Set = emptySet() + ): MutableMap + + /** + * Validates tags according to the particular rules. + * + * @param tags Tags to validate. + */ + fun validateTags(tags: List): List + + /** + * Validate timings according to the particular rules. + * + * @param timings Timings to validate. + */ + fun validateTimings(timings: Map): MutableMap +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/constraints/DatadogDataConstraints.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/constraints/DatadogDataConstraints.kt new file mode 100644 index 0000000000..43a945e1a9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/constraints/DatadogDataConstraints.kt @@ -0,0 +1,221 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.constraints + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.constraints.StringTransform +import com.datadog.android.core.internal.utils.toMutableMap +import java.util.Locale + +/** + * Data constraints validator per Datadog requirements. + * + * @param internalLogger Internal logger. + */ +class DatadogDataConstraints(private val internalLogger: InternalLogger) : DataConstraints { + + // region DataConstraints + + /** @inheritdoc */ + override fun validateTags(tags: List): List { + val convertedTags = tags.mapNotNull { + val tag = convertTag(it) + if (tag == null) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "\"$it\" is an invalid tag, and was ignored." } + ) + } else if (tag != it) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { "tag \"$it\" was modified to \"$tag\" to match our constraints." }, + onlyOnce = true + ) + } + tag + } + val discardedCount = convertedTags.size - MAX_TAG_COUNT + if (discardedCount > 0) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { "too many tags were added, $discardedCount had to be discarded." } + ) + } + return convertedTags.take(MAX_TAG_COUNT) + } + + /** @inheritdoc */ + override fun validateAttributes( + attributes: Map, + keyPrefix: String?, + attributesGroupName: String?, + reservedKeys: Set + ): MutableMap { + // prefix = "a.b" => dotCount = 1+1 ("a.b." + key) + val prefixDotCount = keyPrefix?.let { it.count { character -> character == '.' } + 1 } ?: 0 + val convertedAttributes = attributes.mapNotNull { + // We need this in case the attributes are added from JAVA code and a null key may be + // passed. + @Suppress("SENSELESS_COMPARISON") + if (it.key == null) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "\"$it\" is an invalid attribute, and was ignored." } + ) + null + } else if (it.key in reservedKeys) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "\"$it\" key was in the reservedKeys set, and was dropped." } + ) + null + } else { + val key = convertAttributeKey(it.key, prefixDotCount) + if (key != it.key) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { + "Key \"${it.key}\" " + + "was modified to \"$key\" to match our constraints." + } + ) + } + key to it.value + } + } + val discardedCount = convertedAttributes.size - MAX_ATTR_COUNT + if (discardedCount > 0) { + val warningMessage = resolveDiscardedAttrsWarning( + attributesGroupName, + discardedCount + ) + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { warningMessage } + ) + } + return convertedAttributes.take(MAX_ATTR_COUNT).toMutableMap() + } + + /** @inheritdoc */ + override fun validateTimings(timings: Map): MutableMap { + return timings.mapKeys { entry -> + val sanitizedKey = + entry.key.replace(Regex("[^a-zA-Z0-9\\-_.@$]"), "_") + if (sanitizedKey != entry.key) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { + CUSTOM_TIMING_KEY_REPLACED_WARNING.format( + Locale.US, + entry.key, + sanitizedKey + ) + } + ) + } + sanitizedKey + }.toMutableMap() + } + + private fun resolveDiscardedAttrsWarning( + attributesGroupName: String?, + discardedCount: Int + ): String { + return if (attributesGroupName != null) { + "Too many attributes were added for [$attributesGroupName], " + + "$discardedCount had to be discarded." + } else { + "Too many attributes were added, " + + "$discardedCount had to be discarded." + } + } + + // endregion + + // region Internal/Tag + + @Suppress("UnsafeThirdPartyFunctionCall") // substring IndexOutOfBounds is impossible here + private val tagTransforms = listOf( + // Tags must be lowercase + { it.lowercase(Locale.US) }, + // Tags must start with a letter + { if (it.getOrNull(0) !in 'a'..'z') null else it }, + // Tags convert illegal characters to underscore + { it.replace(Regex("[^a-z0-9_:./-]"), "_") }, + // Tags cannot end with a colon + { if (it.endsWith(':')) it.substring(0, it.lastIndex) else it }, + // Tags can be up to 200 characters long + { if (it.length > MAX_TAG_LENGTH) it.substring(0, MAX_TAG_LENGTH) else it }, + // Dismiss tags with reserved keys + { if (isKeyReserved(it)) null else it } + ) + + private fun convertTag(rawTag: String?): String? { + return tagTransforms.fold(rawTag) { tag, transform -> + @Suppress("UnsafeThirdPartyFunctionCall") // internal safe call + if (tag == null) null else transform.invoke(tag) + } + } + + @Suppress("UnsafeThirdPartyFunctionCall") // substring IndexOutOfBounds is impossible here + private fun isKeyReserved(tag: String): Boolean { + val firstColon = tag.indexOf(':') + return if (firstColon > 0) { + val key = tag.substring(0, firstColon) + key in reservedTagKeys + } else { + false + } + } + + // endregion + + // region Internal/Attribute + + private fun convertAttributeKey(rawKey: String, prefixDotCount: Int): String { + var dotCount = prefixDotCount + val mapped = rawKey.map { + if (it == '.') { + dotCount++ + if (dotCount > MAX_DEPTH_LEVEL) '_' else it + } else { + it + } + } + return String(mapped.toCharArray()) + } + + // endregion + + internal companion object { + + private const val MAX_TAG_LENGTH = 200 + private const val MAX_TAG_COUNT = 100 + + private const val MAX_ATTR_COUNT = 128 + private const val MAX_DEPTH_LEVEL = 9 + + internal const val CUSTOM_TIMING_KEY_REPLACED_WARNING = "Invalid timing name: %s," + + " sanitized to: %s" + + private val reservedTagKeys = setOf( + "host", + "device", + "source", + "service" + ) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/feature/event/JvmCrash.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/feature/event/JvmCrash.kt new file mode 100644 index 0000000000..992297550a --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/feature/event/JvmCrash.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.feature.event + +/** + * JVM crash information to be propagated on the message bus to the features doing crash reporting. + * Is not a part of official API. + */ +@Suppress("UndocumentedPublicClass", "UndocumentedPublicProperty") +sealed class JvmCrash { + abstract val throwable: Throwable + abstract val message: String + abstract val threads: List + + data class Rum( + override val throwable: Throwable, + override val message: String, + override val threads: List + ) : JvmCrash() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/feature/event/ThreadDump.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/feature/event/ThreadDump.kt new file mode 100644 index 0000000000..3681c43fb9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/feature/event/ThreadDump.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.feature.event + +/** + * Thread information to be propagated on the message bus to the features doing crash reporting. + * Is not a part of official API. + */ +@Suppress("UndocumentedPublicClass", "UndocumentedPublicProperty") +data class ThreadDump( + val name: String, + val state: String, + val stack: String, + val crashed: Boolean +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/ContextProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/ContextProvider.kt new file mode 100644 index 0000000000..8bb3c967cd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/ContextProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.Feature + +internal interface ContextProvider { + // TODO RUM-3784 lifecycle checks may be needed for the cases when context is requested + // when datadog is not initialized yet/anymore (case of UploadWorker, other calls site + // should be in sync with lifecycle) + /** + * @param withFeatureContexts Feature contexts ([DatadogContext.featuresContext] property) to include + * in the [DatadogContext] provided. The value should be the feature names as declared by [Feature.name]. + * Default is empty, meaning that no feature contexts will be included. + */ + fun getContext(withFeatureContexts: Set): DatadogContext +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt new file mode 100644 index 0000000000..4bbc7be97d --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -0,0 +1,781 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Process +import androidx.annotation.RequiresApi +import androidx.annotation.WorkerThread +import com.datadog.android.BuildConfig +import com.datadog.android.Datadog +import com.datadog.android.DatadogSite +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.core.configuration.BatchProcessingLevel +import com.datadog.android.core.configuration.BatchSize +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.configuration.UploadFrequency +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.account.DatadogAccountInfoProvider +import com.datadog.android.core.internal.account.MutableAccountInfoProvider +import com.datadog.android.core.internal.account.NoOpMutableAccountInfoProvider +import com.datadog.android.core.internal.data.upload.CurlInterceptor +import com.datadog.android.core.internal.data.upload.GzipRequestInterceptor +import com.datadog.android.core.internal.data.upload.RotatingDnsResolver +import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver +import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider +import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider +import com.datadog.android.core.internal.net.info.NetworkInfoProvider +import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider +import com.datadog.android.core.internal.persistence.JsonObjectDeserializer +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileWriter +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.readTextSafe +import com.datadog.android.core.internal.persistence.file.writeTextSafe +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.internal.privacy.NoOpConsentProvider +import com.datadog.android.core.internal.privacy.TrackingConsentProvider +import com.datadog.android.core.internal.system.AndroidInfoProvider +import com.datadog.android.core.internal.system.AppVersionProvider +import com.datadog.android.core.internal.system.BroadcastReceiverSystemInfoProvider +import com.datadog.android.core.internal.system.DefaultAndroidInfoProvider +import com.datadog.android.core.internal.system.DefaultAppVersionProvider +import com.datadog.android.core.internal.system.NoOpAndroidInfoProvider +import com.datadog.android.core.internal.system.NoOpAppVersionProvider +import com.datadog.android.core.internal.system.NoOpSystemInfoProvider +import com.datadog.android.core.internal.system.SystemInfoProvider +import com.datadog.android.core.internal.thread.BackPressureExecutorService +import com.datadog.android.core.internal.thread.BackPressuredBlockingQueue +import com.datadog.android.core.internal.thread.DatadogThreadFactory +import com.datadog.android.core.internal.thread.LoggingScheduledThreadPoolExecutor +import com.datadog.android.core.internal.thread.ScheduledExecutorServiceFactory +import com.datadog.android.core.internal.time.AppStartTimeProvider +import com.datadog.android.core.internal.time.DatadogNtpEndpoint +import com.datadog.android.core.internal.time.KronosTimeProvider +import com.datadog.android.core.internal.time.LoggingSyncListener +import com.datadog.android.core.internal.user.DatadogUserInfoProvider +import com.datadog.android.core.internal.user.MutableUserInfoProvider +import com.datadog.android.core.internal.user.NoOpMutableUserInfoProvider +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.core.internal.utils.unboundInternalLogger +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.core.thread.FlushableExecutorService +import com.datadog.android.internal.time.DefaultTimeProvider +import com.datadog.android.internal.time.TimeProvider +import com.datadog.android.internal.utils.allowThreadDiskReads +import com.datadog.android.ndk.internal.DatadogNdkCrashHandler +import com.datadog.android.ndk.internal.NdkCrashHandler +import com.datadog.android.ndk.internal.NdkCrashLogDeserializer +import com.datadog.android.ndk.internal.NoOpNdkCrashHandler +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.security.Encryption +import com.google.gson.JsonObject +import com.lyft.kronos.AndroidClockFactory +import com.lyft.kronos.KronosClock +import okhttp3.Call +import okhttp3.CipherSuite +import okhttp3.ConnectionSpec +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.TlsVersion +import java.io.File +import java.io.FileNotFoundException +import java.lang.ref.WeakReference +import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +@Suppress("TooManyFunctions") +internal class CoreFeature( + private val internalLogger: InternalLogger, + private val appStartTimeProvider: AppStartTimeProvider, + private val executorServiceFactory: FlushableExecutorService.Factory, + private val scheduledExecutorServiceFactory: ScheduledExecutorServiceFactory +) { + + internal class OkHttpCallFactory(factory: () -> OkHttpClient) : Call.Factory { + val okhttpClient by lazy(factory) + + override fun newCall(request: Request): Call { + return okhttpClient.newCall(request) + } + } + + internal val initialized = AtomicBoolean(false) + internal var contextRef: WeakReference = WeakReference(null) + + internal var firstPartyHostHeaderTypeResolver = + DefaultFirstPartyHostHeaderTypeResolver(emptyMap()) + internal var networkInfoProvider: NetworkInfoProvider = NoOpNetworkInfoProvider() + internal var systemInfoProvider: SystemInfoProvider = NoOpSystemInfoProvider() + internal var timeProvider: TimeProvider = DefaultTimeProvider() + internal var trackingConsentProvider: ConsentProvider = NoOpConsentProvider() + internal var userInfoProvider: MutableUserInfoProvider = NoOpMutableUserInfoProvider() + internal var accountInfoProvider: MutableAccountInfoProvider = NoOpMutableAccountInfoProvider() + internal var contextProvider: ContextProvider = NoOpContextProvider() + internal var packageVersionProvider: AppVersionProvider = NoOpAppVersionProvider() + internal var androidInfoProvider: AndroidInfoProvider = NoOpAndroidInfoProvider() + + internal lateinit var callFactory: OkHttpCallFactory + internal var kronosClock: KronosClock? = null + + @Volatile + internal var clientToken: String = "" + + @Volatile + internal var serviceName: String = "" + + @Volatile + internal var sourceName: String = DEFAULT_SOURCE_NAME + + @Volatile + internal var sdkVersion: String = DEFAULT_SDK_VERSION + + @Volatile + internal var isMainProcess: Boolean = true + + @Volatile + internal var envName: String = "" + + @Volatile + internal var variant: String = "" + internal var batchSize: BatchSize = BatchSize.MEDIUM + internal var uploadFrequency: UploadFrequency = UploadFrequency.AVERAGE + internal var batchProcessingLevel: BatchProcessingLevel = BatchProcessingLevel.MEDIUM + internal var ndkCrashHandler: NdkCrashHandler = NoOpNdkCrashHandler() + + @Volatile + internal var site: DatadogSite = DatadogSite.US1 + + @Volatile + internal var appBuildId: String? = null + internal var customUploadSchedulerStrategy: UploadSchedulerStrategy? = null + + internal lateinit var uploadExecutorService: ScheduledThreadPoolExecutor + internal lateinit var persistenceExecutorService: FlushableExecutorService + internal lateinit var contextExecutorService: ThreadPoolExecutor + internal lateinit var backpressureStrategy: BackPressureStrategy + + internal var localDataEncryption: Encryption? = null + internal var persistenceStrategyFactory: PersistenceStrategy.Factory? = null + internal lateinit var storageDir: File + + internal val appStartTimeNs: Long + get() = appStartTimeProvider.appStartTimeNs + + // lazy here on purpose: we need to read it only once, even if it is used in different features + @get:WorkerThread + internal val lastViewEvent: JsonObject? by lazy { + // TODO RUM-1462 address Thread safety + @Suppress("ThreadSafety") // called in worker thread context + val viewEvent = readLastViewEvent() + if (viewEvent != null) { + @Suppress("ThreadSafety") // called in worker thread context + deleteLastViewEvent() + } + viewEvent + } + + @get:WorkerThread + private val lastViewEventFile: File by lazy { File(storageDir, LAST_RUM_VIEW_EVENT_FILE_NAME) } + private val lastViewEventFileWriter: FileWriter by lazy { + BatchFileReaderWriter.create( + internalLogger = internalLogger, + encryption = localDataEncryption + ) + } + + internal val lastFatalAnrSent: Long? + get() { + val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME) + return if (file.existsSafe(internalLogger)) { + file.readTextSafe(Charsets.UTF_8, internalLogger)?.toLongOrNull() + } else { + null + } + } + + fun initialize( + appContext: Context, + sdkInstanceId: String, + configuration: Configuration, + consent: TrackingConsent + ) { + if (initialized.get()) { + return + } + readConfigurationSettings(configuration.coreConfig) + readApplicationInformation(appContext, configuration) + resolveProcessInfo(appContext) + setupExecutors() + persistenceExecutorService.executeSafe("NTP Sync initialization", unboundInternalLogger) { + // Kronos performs I/O operation on startup, it needs to run in background + initializeClockSync(appContext) + } + setupOkHttpClient(configuration.coreConfig) + firstPartyHostHeaderTypeResolver + .addKnownHostsWithHeaderTypes(configuration.coreConfig.firstPartyHostsWithHeaderTypes) + androidInfoProvider = DefaultAndroidInfoProvider(appContext) + + storageDir = allowThreadDiskReads { + File( + appContext.cacheDir, + DATADOG_STORAGE_DIR_NAME.format(Locale.US, sdkInstanceId) + ) + } + + // BIG NOTE !! + // Please do not move the block bellow. + // The NDK crash handler `prepareData` function needs to be called exactly at this moment + // to make sure it is the first task that goes in the persistence ExecutorService. + // Because all our persisting components are working asynchronously this will avoid + // having corrupted data (data from previous process over - written in this process into the + // ndk crash folder before the crash was actually handled) + val nativeSourceOverride = configuration.additionalConfig[Datadog.DD_NATIVE_SOURCE_TYPE] as? String + prepareNdkCrashData(nativeSourceOverride) + setupInfoProviders(appContext, consent) + initialized.set(true) + } + + fun stop() { + if (initialized.get()) { + contextRef.get()?.let { + networkInfoProvider.unregister(it) + systemInfoProvider.unregister(it) + } + contextRef.clear() + + trackingConsentProvider.unregisterAllCallbacks() + + cleanupApplicationInfo() + cleanupProviders() + shutDownExecutors() + + try { + kronosClock?.shutdown() + } catch (ise: IllegalStateException) { + // this may be called from the test + // when Kronos is already shut down + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Trying to shut down Kronos when it is already not running" }, + ise + ) + } + + initialized.set(false) + ndkCrashHandler = NoOpNdkCrashHandler() + trackingConsentProvider = NoOpConsentProvider() + } + } + + fun buildFilePersistenceConfig(): FilePersistenceConfig { + return FilePersistenceConfig( + recentDelayMs = batchSize.windowDurationMs + ) + } + + fun createExecutorService(executorContext: String): ExecutorService { + return executorServiceFactory.create(internalLogger, executorContext, backpressureStrategy) + } + + fun createScheduledExecutorService(executorContext: String): ScheduledExecutorService { + return scheduledExecutorServiceFactory.create(internalLogger, executorContext, backpressureStrategy) + } + + @Throws(UnsupportedOperationException::class, InterruptedException::class) + @Suppress("UnsafeThirdPartyFunctionCall") // Used in Nightly tests only + fun drainAndShutdownExecutors() { + val contextTasks = arrayListOf() + contextExecutorService.queue.drainTo(contextTasks) + + contextExecutorService.shutdown() + contextExecutorService.awaitTermination(DRAIN_WAIT_SECONDS, TimeUnit.SECONDS) + contextTasks.forEach { + it.run() + } + + // we need to make sure we drain the runnable list in both executors first + // then we shut them down by using the await termination method to make sure we block + // the thread until the active task is finished. + val ioTasks = arrayListOf() + persistenceExecutorService.drainTo(ioTasks) + + uploadExecutorService + .queue + .drainTo(ioTasks) + persistenceExecutorService.shutdown() + uploadExecutorService.shutdown() + + persistenceExecutorService.awaitTermination(DRAIN_WAIT_SECONDS, TimeUnit.SECONDS) + uploadExecutorService.awaitTermination(DRAIN_WAIT_SECONDS, TimeUnit.SECONDS) + + ioTasks.forEach { + it.run() + } + } + + // region Internal + + @WorkerThread + internal fun writeLastViewEvent(data: ByteArray) { + lastViewEventFileWriter.writeData(lastViewEventFile, RawBatchEvent(data), false) + } + + @WorkerThread + internal fun deleteLastViewEvent() { + if (lastViewEventFile.existsSafe(internalLogger)) { + lastViewEventFile.deleteSafe(internalLogger) + } else { + @Suppress("DEPRECATION") + val legacyViewEventFile = DatadogNdkCrashHandler.getLastViewEventFile(storageDir) + if (legacyViewEventFile.existsSafe(internalLogger)) { + legacyViewEventFile.deleteSafe(internalLogger) + } + } + } + + @WorkerThread + internal fun writeLastFatalAnrSent(anrTimestamp: Long) { + // TODO RUM-3790 this is temporary solution for storing just a timestamp, later we will + // migrate to a dedicated data store solution (same applies to the last RUM view event) + val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME) + file.writeTextSafe(anrTimestamp.toString(), Charsets.UTF_8, internalLogger) + } + + @WorkerThread + internal fun deleteLastFatalAnrSent() { + val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME) + if (file.existsSafe(internalLogger)) { + file.deleteSafe(internalLogger) + } + } + + @WorkerThread + private fun readLastViewEvent(): JsonObject? { + val lastViewEventFile = if (lastViewEventFile.existsSafe(internalLogger)) { + lastViewEventFile + } else { + @Suppress("DEPRECATION") + val legacyViewEventFile = DatadogNdkCrashHandler.getLastViewEventFile(storageDir) + if (legacyViewEventFile.existsSafe(internalLogger)) { + legacyViewEventFile + } else { + null + } + } + + if (lastViewEventFile == null) return null + + val reader = + BatchFileReaderWriter.create(internalLogger, localDataEncryption) + val content = reader.readData(lastViewEventFile) + return if (content.isEmpty()) { + null + } else { + @Suppress("UnsafeThirdPartyFunctionCall") // safe to call last, collection is not empty + String(content.last().data, Charsets.UTF_8).run { + JsonObjectDeserializer(internalLogger).deserialize(this) + } + } + } + + private fun prepareNdkCrashData(nativeSourceType: String?) { + if (isMainProcess) { + ndkCrashHandler = DatadogNdkCrashHandler( + storageDir, + persistenceExecutorService, + NdkCrashLogDeserializer(internalLogger), + internalLogger, + lastRumViewEventProvider = { lastViewEvent }, + nativeCrashSourceType = nativeSourceType ?: "ndk" + ) + ndkCrashHandler.prepareData() + } + } + + @WorkerThread + private fun initializeClockSync(appContext: Context) { + val safeContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + getSafeContext(appContext) + } else { + appContext + } + kronosClock = AndroidClockFactory.createKronosClock( + safeContext, + ntpHosts = listOf( + DatadogNtpEndpoint.NTP_0, + DatadogNtpEndpoint.NTP_1, + DatadogNtpEndpoint.NTP_2, + DatadogNtpEndpoint.NTP_3 + ).map { it.host }, + cacheExpirationMs = TimeUnit.MINUTES.toMillis(NTP_CACHE_EXPIRATION_MINUTES), + minWaitTimeBetweenSyncMs = TimeUnit.MINUTES.toMillis(NTP_DELAY_BETWEEN_SYNCS_MINUTES), + syncListener = LoggingSyncListener(internalLogger) + ).apply { + if (!disableKronosBackgroundSync) { + try { + syncInBackground() + } catch (ise: IllegalStateException) { + // should never happen + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Unable to launch a synchronize local time with an NTP server." }, + ise + ) + } + } + + timeProvider = KronosTimeProvider(this) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun getSafeContext(appContext: Context): Context { + // When the host app uses the `directBootAware` flag on a file encrypted device, + // the app can wake up during the boot sequence before the device is unlocked + // This mean any file I/O or access to shared preferences will throw an exception + // This safe context creates a device-protected storage which can be used for non sensitive + // data. It should not be used to store the data captured by the SDK. + return appContext.createDeviceProtectedStorageContext() ?: appContext + } + + private fun readApplicationInformation(appContext: Context, configuration: Configuration) { + packageVersionProvider = DefaultAppVersionProvider( + getPackageInfo(appContext)?.let { + // we need to use the deprecated method because getLongVersionCode method is only + // available from API 28 and above + @Suppress("DEPRECATION") + it.versionName ?: it.versionCode.toString() + } ?: DEFAULT_APP_VERSION + ) + clientToken = configuration.clientToken + serviceName = configuration.service ?: appContext.packageName + envName = configuration.env + variant = configuration.variant + appBuildId = readBuildId(appContext) + + contextRef = WeakReference(appContext) + } + + private fun getPackageInfo(appContext: Context): PackageInfo? { + return try { + val packageName = appContext.packageName + with(appContext.packageManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) + } else { + getPackageInfo(packageName, 0) + } + } + } catch (e: PackageManager.NameNotFoundException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Unable to read your application's version name" }, + e + ) + null + } + } + + private fun readBuildId(context: Context): String? { + return with(context.assets) { + try { + open(BUILD_ID_FILE_NAME).bufferedReader().use { + it.readText().trim() + } + } catch (@Suppress("SwallowedException") e: FileNotFoundException) { + internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + { BUILD_ID_IS_MISSING_INFO_MESSAGE } + ) + null + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + internalLogger.log( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { BUILD_ID_READ_ERROR }, + e + ) + null + } + } + } + + private fun readConfigurationSettings(configuration: Configuration.Core) { + batchSize = configuration.batchSize + uploadFrequency = configuration.uploadFrequency + localDataEncryption = configuration.encryption + persistenceStrategyFactory = configuration.persistenceStrategyFactory + site = configuration.site + backpressureStrategy = configuration.backpressureStrategy + customUploadSchedulerStrategy = configuration.uploadSchedulerStrategy + } + + private fun setupInfoProviders( + appContext: Context, + consent: TrackingConsent + ) { + // Tracking Consent Provider + trackingConsentProvider = TrackingConsentProvider(consent) + + // System Info Provider + systemInfoProvider = BroadcastReceiverSystemInfoProvider(internalLogger = internalLogger) + systemInfoProvider.register(appContext) + + // Network Info Provider + setupNetworkInfoProviders(appContext) + + // User Info Provider + userInfoProvider = DatadogUserInfoProvider() + + // Account Info Provider + accountInfoProvider = DatadogAccountInfoProvider(internalLogger) + } + + private fun setupNetworkInfoProviders(appContext: Context) { + networkInfoProvider = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + CallbackNetworkInfoProvider(internalLogger = internalLogger) + } else { + BroadcastReceiverNetworkInfoProvider() + } + networkInfoProvider.register(appContext) + } + + @Suppress("SpreadOperator") + private fun setupOkHttpClient(configuration: Configuration.Core) { + callFactory = OkHttpCallFactory { + val connectionSpec = when { + configuration.needsClearTextHttp -> ConnectionSpec.CLEARTEXT + else -> ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS) + .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3) + .cipherSuites(*RESTRICTED_CIPHER_SUITES) + .build() + } + + val builder = OkHttpClient.Builder() + builder.callTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .writeTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) + .connectionSpecs(listOf(connectionSpec)) + + if (BuildConfig.DEBUG) { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + builder.addNetworkInterceptor(CurlInterceptor()) + } else { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + builder.addInterceptor(GzipRequestInterceptor(internalLogger)) + } + + if (configuration.proxy != null) { + builder.proxy(configuration.proxy) + builder.proxyAuthenticator(configuration.proxyAuth) + } + + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + builder.dns(RotatingDnsResolver()) + + builder.build() + } + } + + private fun setupExecutors() { + uploadExecutorService = LoggingScheduledThreadPoolExecutor( + corePoolSize = CORE_DEFAULT_POOL_SIZE, + executorContext = "upload", + logger = internalLogger, + backPressureStrategy = backpressureStrategy + ) + persistenceExecutorService = executorServiceFactory.create( + internalLogger = internalLogger, + executorContext = "storage", + backPressureStrategy = backpressureStrategy + ) + val contextQueue = BackPressuredBlockingQueue( + internalLogger, + executorContext = "context", + capacity = Int.MAX_VALUE, + notifyThreshold = 1024, + // just notify when reached + onItemDropped = {}, + onThresholdReached = {}, + backpressureMitigation = null + ) + @Suppress("UnsafeThirdPartyFunctionCall") // all parameters are safe + contextExecutorService = ThreadPoolExecutor( + // core pool size + 1, + // max pool size, + 1, + // keep-alive time + 0L, + TimeUnit.MILLISECONDS, + contextQueue, + DatadogThreadFactory("context") + ) + } + + private fun resolveProcessInfo(appContext: Context) { + val currentProcessId = Process.myPid() + val manager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + val currentProcess = manager?.runningAppProcesses?.firstOrNull { + it.pid == currentProcessId + } + isMainProcess = if (currentProcess == null) { + true + } else { + appContext.packageName == currentProcess.processName + } + if (!isMainProcess) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { SDK_INITIALIZED_IN_SECONDARY_PROCESS_WARNING_MESSAGE } + ) + } + } + + private fun shutDownExecutors() { + uploadExecutorService.shutdownNow() + contextExecutorService.shutdownNow() + persistenceExecutorService.shutdownNow() + + try { + uploadExecutorService.awaitTermination(1, TimeUnit.SECONDS) + contextExecutorService.awaitTermination(1, TimeUnit.SECONDS) + persistenceExecutorService.awaitTermination(1, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + try { + // Restore the interrupted status + Thread.currentThread().interrupt() + } catch (se: SecurityException) { + // this should not happen + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Thread was unable to set its own interrupted state" }, + se + ) + } + } + } + + private fun cleanupApplicationInfo() { + clientToken = "" + packageVersionProvider = NoOpAppVersionProvider() + serviceName = "" + sourceName = DEFAULT_SOURCE_NAME + sdkVersion = DEFAULT_SDK_VERSION + isMainProcess = true + envName = "" + variant = "" + } + + private fun cleanupProviders() { + firstPartyHostHeaderTypeResolver = DefaultFirstPartyHostHeaderTypeResolver(emptyMap()) + networkInfoProvider = NoOpNetworkInfoProvider() + systemInfoProvider = NoOpSystemInfoProvider() + timeProvider = DefaultTimeProvider() + trackingConsentProvider = NoOpConsentProvider() + userInfoProvider = NoOpMutableUserInfoProvider() + androidInfoProvider = NoOpAndroidInfoProvider() + } + + // endregion + + companion object { + internal const val SDK_INITIALIZED_IN_SECONDARY_PROCESS_WARNING_MESSAGE = + "Datadog SDK was initialized in a secondary process: although data will still be captured," + + " nothing will be uploaded from this process. Make sure to also initialize the SDK from the main" + + " process of your application." + + internal val DEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY = + FlushableExecutorService.Factory { logger, executorContext, backPressureStrategy -> + BackPressureExecutorService(logger, executorContext, backPressureStrategy) + } + + internal val DEFAULT_SCHEDULED_EXECUTOR_SERVICE_FACTORY = + ScheduledExecutorServiceFactory { logger, executorContext, backPressureStrategy -> + LoggingScheduledThreadPoolExecutor(1, executorContext, logger, backPressureStrategy) + } + + // region Constants + + internal val NETWORK_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(45) + private const val CORE_DEFAULT_POOL_SIZE = 1 // Only one thread will be kept alive + internal const val DATADOG_STORAGE_DIR_NAME = "datadog-%s" + + // this is a default source to be used when uploading RUM/Logs/Span data, however there is a + // possibility to override it which is useful when SDK is used via bridge, say + // from React Native integration + internal const val DEFAULT_SOURCE_NAME = "android" + internal const val DEFAULT_SDK_VERSION = BuildConfig.SDK_VERSION_NAME + internal const val DEFAULT_APP_VERSION = "?" + + internal const val LAST_RUM_VIEW_EVENT_FILE_NAME = "last_view_event" + internal const val LAST_FATAL_ANR_SENT_FILE_NAME = "last_fatal_anr_sent" + + // should be the same as in dd-sdk-android-gradle-plugin + internal const val BUILD_ID_FILE_NAME = "datadog.buildId" + internal const val BUILD_ID_IS_MISSING_INFO_MESSAGE = + "Build ID is not found in the application" + + " assets. If you are using obfuscation, please use Datadog Gradle Plugin 1.13.0" + + " or above to be able to de-obfuscate stacktraces." + internal const val BUILD_ID_READ_ERROR = + "Failed to read Build ID information, de-obfuscation may not work properly." + + internal val RESTRICTED_CIPHER_SUITES = arrayOf( + // TLS 1.3 + + // these 3 are mandatory to implement by TLS 1.3 RFC + // https://datatracker.ietf.org/doc/html/rfc8446#section-9.1 + CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + + // TLS 1.2 + + // these 4 are FIPS 140-2 compliant by OpenSSL + + // GOV DC supports only that one and below + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + ) + + const val DRAIN_WAIT_SECONDS = 10L + const val NTP_CACHE_EXPIRATION_MINUTES = 30L + const val NTP_DELAY_BETWEEN_SYNCS_MINUTES = 5L + + // TESTS ONLY, to prevent Kronos spinning sync threads in unit-tests, otherwise + // LoggingSyncListener can interact with internalLogger, breaking mockito + // verification expectations. + // TODO RUM-3791 isolate Kronos somehow for unit-tests + internal var disableKronosBackgroundSync = false + + // endregion + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogContextProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogContextProvider.kt new file mode 100644 index 0000000000..f60a0b5bd8 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogContextProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.DeviceInfo +import com.datadog.android.api.context.LocaleInfo +import com.datadog.android.api.context.ProcessInfo +import com.datadog.android.api.context.TimeInfo +import java.util.concurrent.TimeUnit + +internal class DatadogContextProvider( + private val coreFeature: CoreFeature, + private val featureContextProvider: FeatureContextProvider +) : ContextProvider { + @Suppress("LongMethod") + override fun getContext(withFeatureContexts: Set): DatadogContext { + // IMPORTANT All properties should be immutable and be frozen at the state + // of the context construction moment + return DatadogContext( + site = coreFeature.site, + clientToken = coreFeature.clientToken, + service = coreFeature.serviceName, + env = coreFeature.envName, + version = coreFeature.packageVersionProvider.version, + variant = coreFeature.variant, + sdkVersion = coreFeature.sdkVersion, + source = coreFeature.sourceName, + time = with(coreFeature.timeProvider) { + val deviceTimeMs = getDeviceTimestamp() + val serverTimeMs = getServerTimestamp() + TimeInfo( + deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(deviceTimeMs), + serverTimeNs = TimeUnit.MILLISECONDS.toNanos(serverTimeMs), + serverTimeOffsetNs = TimeUnit.MILLISECONDS + .toNanos(serverTimeMs - deviceTimeMs), + serverTimeOffsetMs = serverTimeMs - deviceTimeMs + ) + }, + processInfo = ProcessInfo( + isMainProcess = coreFeature.isMainProcess + ), + networkInfo = coreFeature.networkInfoProvider.getLatestNetworkInfo(), + deviceInfo = with(coreFeature.androidInfoProvider) { + DeviceInfo( + deviceName = deviceName, + deviceBrand = deviceBrand, + deviceType = deviceType, + deviceModel = deviceModel, + deviceBuildId = deviceBuildId, + osName = osName, + osVersion = osVersion, + osMajorVersion = osMajorVersion, + architecture = architecture, + numberOfDisplays = numberOfDisplays, + localeInfo = with(coreFeature.androidInfoProvider) { + LocaleInfo( + locales = locales, + currentLocale = currentLocale, + timeZone = timeZone + ) + } + ) + }, + userInfo = coreFeature.userInfoProvider.getUserInfo(), + accountInfo = coreFeature.accountInfoProvider.getAccountInfo(), + trackingConsent = coreFeature.trackingConsentProvider.getConsent(), + appBuildId = coreFeature.appBuildId, + featuresContext = mutableMapOf>().apply { + withFeatureContexts.forEach { + val featureContext = featureContextProvider.getFeatureContext(it) + if (featureContext.isNotEmpty()) { + this[it] = featureContext + } + } + } + ) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt new file mode 100644 index 0000000000..ef104c8ef5 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import android.app.Application +import android.content.Context +import android.content.pm.ApplicationInfo +import android.os.Build +import android.util.Log +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureContextUpdateReceiver +import com.datadog.android.api.feature.FeatureEventReceiver +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.configuration.BatchSize +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.configuration.UploadFrequency +import com.datadog.android.core.internal.lifecycle.ProcessLifecycleCallback +import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor +import com.datadog.android.core.internal.logger.SdkInternalLogger +import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.core.internal.system.BuildSdkVersionProvider +import com.datadog.android.core.internal.time.DefaultAppStartTimeProvider +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.core.internal.utils.getSafe +import com.datadog.android.core.internal.utils.scheduleSafe +import com.datadog.android.core.internal.utils.submitSafe +import com.datadog.android.core.thread.FlushableExecutorService +import com.datadog.android.error.internal.CrashReportsFeature +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.android.privacy.TrackingConsent +import com.google.gson.JsonObject +import java.io.File +import java.util.Collections +import java.util.Locale +import java.util.UUID +import java.util.concurrent.Callable +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Lock + +/** + * Internal implementation of the [SdkCore] interface. + * @param context the application's Android [Context] + * @param instanceId the unique identifier for this instance + * @param name the name of this instance + * @param internalLoggerProvider Provider for [InternalLogger] instance. + * @param executorServiceFactory Custom factory for executors, used only in unit-tests + * @param buildSdkVersionProvider Build.VERSION.SDK_INT provider used for the test + */ +@Suppress("TooManyFunctions") +internal class DatadogCore( + context: Context, + internal val instanceId: String, + override val name: String, + internalLoggerProvider: (FeatureSdkCore) -> InternalLogger = { SdkInternalLogger(it) }, + // only for unit tests + private val executorServiceFactory: FlushableExecutorService.Factory? = null, + private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT +) : InternalSdkCore { + + internal lateinit var coreFeature: CoreFeature + + private lateinit var shutdownHook: Thread + + internal val features: MutableMap = ConcurrentHashMap() + + internal val appContext: Context = context.applicationContext + + internal var contextProvider: ContextProvider = NoOpContextProvider() + + internal val isActive: Boolean + get() = coreFeature.initialized.get() + + private var processLifecycleMonitor: ProcessLifecycleMonitor? = null + + @Suppress("UnsafeThirdPartyFunctionCall") // the argument is always empty + internal val featureContextUpdateReceivers: MutableSet = + Collections.newSetFromMap(ConcurrentHashMap()) + + // region SdkCore + + /** @inheritDoc */ + override val time: TimeInfo + get() { + return with(coreFeature.timeProvider) { + val deviceTimeMs = getDeviceTimestamp() + val serverTimeMs = getServerTimestamp() + TimeInfo( + deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(deviceTimeMs), + serverTimeNs = TimeUnit.MILLISECONDS.toNanos(serverTimeMs), + serverTimeOffsetNs = TimeUnit.MILLISECONDS + .toNanos(serverTimeMs - deviceTimeMs), + serverTimeOffsetMs = serverTimeMs - deviceTimeMs + ) + } + } + + /** @inheritDoc */ + override val service: String + get() = coreFeature.serviceName + + /** @inheritDoc */ + override val firstPartyHostResolver: FirstPartyHostHeaderTypeResolver + get() = coreFeature.firstPartyHostHeaderTypeResolver + + /** @inheritDoc */ + override val internalLogger: InternalLogger = internalLoggerProvider(this) + + /** @inheritDoc */ + override var isDeveloperModeEnabled: Boolean = false + internal set + + /** @inheritDoc */ + override fun registerFeature(feature: Feature) { + val sdkFeature = SdkFeature( + coreFeature, + contextProvider, + feature, + internalLogger + ) + features[feature.name] = sdkFeature + sdkFeature.initialize(appContext, instanceId) + + if (feature.name == Feature.RUM_FEATURE_NAME) { + coreFeature.ndkCrashHandler.handleNdkCrash(this) + } + } + + /** @inheritDoc */ + override fun getFeature(featureName: String): FeatureScope? { + return features[featureName] + } + + /** @inheritDoc */ + @AnyThread + override fun setTrackingConsent(consent: TrackingConsent) { + coreFeature.contextExecutorService.executeSafe("DatadogCore.setTrackingConsent", internalLogger) { + coreFeature.trackingConsentProvider.setConsent(consent) + } + } + + /** @inheritDoc */ + @AnyThread + override fun setUserInfo( + id: String, + name: String?, + email: String?, + extraInfo: Map + ) { + val extraInfoSnapshot = extraInfo.toMap() + coreFeature.contextExecutorService.executeSafe("DatadogCore.setUserInfo", internalLogger) { + coreFeature.userInfoProvider.setUserInfo(id, name, email, extraInfoSnapshot) + } + } + + /** @inheritDoc */ + @AnyThread + override fun addUserProperties(extraInfo: Map) { + val extraInfoSnapshot = extraInfo.toMap() + coreFeature.contextExecutorService.executeSafe("DatadogCore.addUserProperties", internalLogger) { + coreFeature.userInfoProvider.addUserProperties(extraInfoSnapshot) + } + } + + /** @inheritDoc */ + @AnyThread + override fun clearUserInfo() { + coreFeature.contextExecutorService.executeSafe("DatadogCore.clearUserInfo", internalLogger) { + coreFeature.userInfoProvider.clearUserInfo() + } + } + + /** @inheritDoc */ + @AnyThread + override fun clearAllData() { + coreFeature.contextExecutorService.executeSafe("DatadogCore.clearAllData", internalLogger) { + features.values.forEach { + it.clearAllData() + } + getPersistenceExecutorService().executeSafe("Clear all data", internalLogger) { + coreFeature.deleteLastViewEvent() + coreFeature.deleteLastFatalAnrSent() + } + } + } + + override fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) { + val extraInfoSnapshot = extraInfo.toMap() + coreFeature.contextExecutorService.executeSafe("DatadogCore.setAccountInfo", internalLogger) { + coreFeature.accountInfoProvider.setAccountInfo(id, name, extraInfoSnapshot) + } + } + + override fun addAccountExtraInfo( + extraInfo: Map + ) { + val extraInfoSnapshot = extraInfo.toMap() + coreFeature.contextExecutorService.executeSafe("DatadogCore.addAccountExtraInfo", internalLogger) { + coreFeature.accountInfoProvider.addExtraInfo(extraInfoSnapshot) + } + } + + override fun clearAccountInfo() { + coreFeature.contextExecutorService.executeSafe("DatadogCore.clearAccountInfo", internalLogger) { + coreFeature.accountInfoProvider.clearAccountInfo() + } + } + + /** @inheritDoc */ + override fun updateFeatureContext( + featureName: String, + useContextThread: Boolean, + updateCallback: (context: MutableMap) -> Unit + ) { + val runnable = runnable@{ + val feature = features[featureName] ?: return@runnable + feature.featureContextLock.writeLock().safeTryWithLock(1, TimeUnit.SECONDS) { + val currentContext = feature.featureContext + updateCallback(currentContext) + featureContextUpdateReceivers.forEach { + it.onContextUpdate(featureName, currentContext) + } + } + } + if (useContextThread) { + coreFeature.contextExecutorService.executeSafe( + "DatadogCore.updateFeatureContext-$featureName", + internalLogger, + runnable + ) + } else { + runnable.invoke() + } + } + + /** @inheritDoc */ + override fun getFeatureContext(featureName: String, useContextThread: Boolean): Map { + val callable = Callable> { + val feature = features[featureName] ?: return@Callable emptyMap() + return@Callable feature.featureContextLock.readLock().safeWithLock { + // Creating copy here is VERY important - this will make + // independent snapshot of the features context which is not affected by the + // changes which can be made later by another thread. + // Use HashMap instead of .toMutableMap() for faster init + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + HashMap(feature.featureContext) + }.orEmpty() + } + return if (useContextThread) { + return coreFeature.contextExecutorService + .submitSafe( + "DatadogCore.getFeatureContext-$featureName", + internalLogger, + callable + ) + .getSafe("DatadogCore.getFeatureContext-$featureName", internalLogger) + .orEmpty() + } else { + @Suppress("UnsafeThirdPartyFunctionCall") // not 3rd party + callable.call() + } + } + + /** @inheritDoc */ + override fun setEventReceiver(featureName: String, receiver: FeatureEventReceiver) { + val feature = features[featureName] + if (feature == null) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { MISSING_FEATURE_FOR_EVENT_RECEIVER.format(Locale.US, featureName) } + ) + } else { + if (feature.eventReceiver.get() != null) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { EVENT_RECEIVER_ALREADY_EXISTS.format(Locale.US, featureName) } + ) + } + feature.eventReceiver.set(receiver) + } + } + + override fun setContextUpdateReceiver(listener: FeatureContextUpdateReceiver) { + // the argument is always non - null, so we can suppress the warning + @Suppress("UnsafeThirdPartyFunctionCall") + if (featureContextUpdateReceivers.contains(listener)) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { CONTEXT_UPDATE_LISTENER_ALREADY_REGISTERED.format(Locale.US, listener) } + ) + } + features.forEach { + val currentContext = getFeatureContext(it.key, false) + if (currentContext.isNotEmpty()) { + listener.onContextUpdate(it.key, currentContext) + } + } + featureContextUpdateReceivers.add(listener) + } + + override fun removeContextUpdateReceiver(listener: FeatureContextUpdateReceiver) { + featureContextUpdateReceivers.remove(listener) + } + + /** @inheritDoc */ + override fun removeEventReceiver(featureName: String) { + features[featureName]?.eventReceiver?.set(null) + } + + /** @inheritDoc */ + override fun createSingleThreadExecutorService(executorContext: String): ExecutorService { + return coreFeature.createExecutorService(executorContext) + } + + /** @inheritDoc */ + override fun createScheduledExecutorService(executorContext: String): ScheduledExecutorService { + return coreFeature.createScheduledExecutorService(executorContext) + } + + override fun setAnonymousId(anonymousId: UUID?) { + coreFeature.contextExecutorService.executeSafe("DatadogCore.setAnonymousId", internalLogger) { + coreFeature.userInfoProvider.setAnonymousId(anonymousId?.toString()) + } + } + + override fun isCoreActive(): Boolean = isActive + + // endregion + + // region InternalSdkCore + + override val networkInfo: NetworkInfo + get() = coreFeature.networkInfoProvider.getLatestNetworkInfo() + + override val trackingConsent: TrackingConsent + get() { + return coreFeature.contextExecutorService.submitSafe( + "getTrackingConsent", + internalLogger, + Callable { + coreFeature.trackingConsentProvider.getConsent() + } + ).getSafe("getTrackingConsent", internalLogger) ?: TrackingConsent.NOT_GRANTED + } + + override val rootStorageDir: File + get() = coreFeature.storageDir + + @get:WorkerThread + override val lastViewEvent: JsonObject? + get() = coreFeature.lastViewEvent + + @get:WorkerThread + override val lastFatalAnrSent: Long? + get() = coreFeature.lastFatalAnrSent + + override val appStartTimeNs: Long + get() = coreFeature.appStartTimeNs + + @WorkerThread + override fun writeLastViewEvent(data: ByteArray) { + // we need to write it only if we are going to read ApplicationExitInfo (available on + // API 30+) or if there is NDK crash tracking enabled + if (buildSdkVersionProvider.version >= Build.VERSION_CODES.R || + features.containsKey(Feature.NDK_CRASH_REPORTS_FEATURE_NAME) + ) { + coreFeature.writeLastViewEvent(data) + } else { + internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.MAINTAINER, + { NO_NEED_TO_WRITE_LAST_VIEW_EVENT } + ) + } + } + + @WorkerThread + override fun deleteLastViewEvent() { + coreFeature.deleteLastViewEvent() + } + + @WorkerThread + override fun writeLastFatalAnrSent(anrTimestamp: Long) { + coreFeature.writeLastFatalAnrSent(anrTimestamp) + } + + override fun getPersistenceExecutorService(): ExecutorService { + return coreFeature.persistenceExecutorService + } + + override fun getAllFeatures(): List { + return features.values.toList() + } + + override fun getDatadogContext(withFeatureContexts: Set): DatadogContext? { + return coreFeature.contextExecutorService + .submitSafe( + "getDatadogContext", + internalLogger, + Callable { + with(contextProvider) { if (this is NoOpContextProvider) null else getContext(withFeatureContexts) } + } + ) + .getSafe("getDatadogContext", internalLogger) + } + + // endregion + + // region Internal + + internal fun initialize(configuration: Configuration) { + if (!isEnvironmentNameValid(configuration.env)) { + @Suppress("ThrowingInternalException") + throw IllegalArgumentException(MESSAGE_ENV_NAME_NOT_VALID) + } + + val isDebug = isAppDebuggable(appContext) + + var mutableConfig = configuration + if (isDebug and configuration.coreConfig.enableDeveloperModeWhenDebuggable) { + mutableConfig = modifyConfigurationForDeveloperDebug(configuration) + isDeveloperModeEnabled = true + Datadog.setVerbosity(Log.VERBOSE) + } + + // always initialize Core Features first + val flushableExecutorServiceFactory = + executorServiceFactory ?: CoreFeature.DEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY + coreFeature = CoreFeature( + internalLogger, + DefaultAppStartTimeProvider(), + flushableExecutorServiceFactory, + CoreFeature.DEFAULT_SCHEDULED_EXECUTOR_SERVICE_FACTORY + ) + coreFeature.initialize( + appContext, + instanceId, + mutableConfig, + TrackingConsent.PENDING + ) + + contextProvider = DatadogContextProvider(coreFeature) { + // useContextThread = false to infer the caller thread (caller is responsible for the thread selection) + getFeatureContext(it, false) + } + + applyAdditionalConfiguration(mutableConfig.additionalConfig) + + if (mutableConfig.crashReportsEnabled) { + initializeCrashReportFeature() + } + + setupLifecycleMonitorCallback(appContext) + + setupShutdownHook() + sendCoreConfigurationTelemetryEvent(configuration) + } + + private fun initializeCrashReportFeature() { + val crashReportsFeature = CrashReportsFeature(this) + registerFeature(crashReportsFeature) + } + + @Suppress("FunctionMaxLength") + private fun modifyConfigurationForDeveloperDebug(configuration: Configuration): Configuration { + return configuration.copy( + coreConfig = configuration.coreConfig.copy( + batchSize = BatchSize.SMALL, + uploadFrequency = UploadFrequency.FREQUENT + ) + ) + } + + @Suppress("ComplexMethod") + private fun applyAdditionalConfiguration( + additionalConfiguration: Map + ) { + // NOTE: be careful with the logic in this method - it is a part of initialization sequence, + // so some things may yet not be initialized -> not accessible, some things may already be + // initialized and be not mutable anymore + additionalConfiguration[Datadog.DD_SOURCE_TAG]?.let { + if (it is String && it.isNotBlank()) { + coreFeature.sourceName = it + } + } + + additionalConfiguration[Datadog.DD_SDK_VERSION_TAG]?.let { + if (it is String && it.isNotBlank()) { + coreFeature.sdkVersion = it + } + } + + additionalConfiguration[Datadog.DD_APP_VERSION_TAG]?.let { + if (it is String && it.isNotBlank()) { + coreFeature.packageVersionProvider.version = it + } + } + } + + private fun setupLifecycleMonitorCallback(appContext: Context) { + if (appContext is Application) { + processLifecycleMonitor = ProcessLifecycleMonitor( + ProcessLifecycleCallback( + appContext, + name, + internalLogger + ) + ).apply { + appContext.registerActivityLifecycleCallbacks(this) + } + } + } + + private fun isEnvironmentNameValid(envName: String): Boolean { + return envName.matches(Regex(ENV_NAME_VALIDATION_REG_EX)) + } + + private fun isAppDebuggable(context: Context): Boolean { + return (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 + } + + private fun setupShutdownHook() { + // Issue #154 (“Thread starting during runtime shutdown”) + // Make sure we stop Datadog when the Runtime shuts down + try { + val hookRunnable = Runnable { stop() } + + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + shutdownHook = Thread(hookRunnable, SHUTDOWN_THREAD_NAME) + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + Runtime.getRuntime().addShutdownHook(shutdownHook) + } catch (e: IllegalStateException) { + // Most probably Runtime is already shutting down + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Unable to add shutdown hook, Runtime is already shutting down" }, + e + ) + stop() + } catch (e: IllegalArgumentException) { + // can only happen if hook is already added, or already running + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Shutdown hook was rejected" }, + e + ) + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Security Manager denied adding shutdown hook " }, + e + ) + } + } + + private fun removeShutdownHook() { + if (this::shutdownHook.isInitialized) { + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook) + } catch (e: IllegalStateException) { + // Most probably Runtime is already shutting down + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Unable to remove shutdown hook, Runtime is already shutting down" }, + e + ) + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Security Manager denied removing shutdown hook " }, + e + ) + } + } + } + + @Suppress("FunctionMaxLength") + private fun sendCoreConfigurationTelemetryEvent(configuration: Configuration) { + val runnable = Runnable { + val rumFeature = getFeature(Feature.RUM_FEATURE_NAME) ?: return@Runnable + + val event = InternalTelemetryEvent.Configuration( + trackErrors = configuration.crashReportsEnabled, + batchSize = configuration.coreConfig.batchSize.windowDurationMs, + useProxy = configuration.coreConfig.proxy != null, + useLocalEncryption = configuration.coreConfig.encryption != null, + batchUploadFrequency = configuration.coreConfig.uploadFrequency.baseStepMs, + batchProcessingLevel = configuration.coreConfig.batchProcessingLevel.maxBatchesPerUploadJob + ) + rumFeature.sendEvent(event) + } + + coreFeature.uploadExecutorService.scheduleSafe( + "Configuration telemetry", + CONFIGURATION_TELEMETRY_DELAY_MS, + TimeUnit.MILLISECONDS, + internalLogger, + runnable + ) + } + + private fun Lock.safeTryWithLock(time: Long, unit: TimeUnit, block: () -> Unit) { + val locked = try { + // NullPointerException cannot happen, time unit is not null + @Suppress("UnsafeThirdPartyFunctionCall") + tryLock(time, unit) + } catch (e: InterruptedException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { "Couldn't acquire ${javaClass.simpleName} due to the exception thrown, aborting operation." }, + e + ) + return + } + if (!locked) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { + "Couldn't acquire ${javaClass.simpleName} due to" + + " timeout ($time $unit), aborting operation." + } + ) + return + } + try { + block() + } finally { + if (locked) { + // IllegalMonitorStateException cannot happen, we check locked flag + @Suppress("UnsafeThirdPartyFunctionCall") + unlock() + } + } + } + + private fun Lock.safeWithLock(block: () -> T): T? { + try { + lock() + } catch (e: InterruptedException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { "Couldn't acquire ${javaClass.simpleName} lock due to the exception thrown, aborting operation." }, + e + ) + return null + } + return try { + block() + } finally { + // IllegalMonitorStateException cannot happen, lock() call above succeeded + @Suppress("UnsafeThirdPartyFunctionCall") + unlock() + } + } + + /** + * Stops all process for this instance of the Datadog SDK. + */ + internal fun stop() { + features.keys.forEach { + features.remove(it)?.stop() + } + + if (appContext is Application && processLifecycleMonitor != null) { + appContext.unregisterActivityLifecycleCallbacks(processLifecycleMonitor) + } + + contextProvider = NoOpContextProvider() + coreFeature.stop() + isDeveloperModeEnabled = false + + removeShutdownHook() + } + + /** + * Flushes all stored data (send everything right now). + */ + @WorkerThread + internal fun flushStoredData() { + // We need to drain and shutdown the executors first to make sure we avoid duplicated + // data due to async operations. + coreFeature.drainAndShutdownExecutors() + + features.values.forEach { + it.flushStoredData() + } + } + + // endregion + + companion object { + internal const val SHUTDOWN_THREAD_NAME = "datadog_shutdown" + + internal const val ENV_NAME_VALIDATION_REG_EX = "[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]" + internal const val MESSAGE_ENV_NAME_NOT_VALID = + "The environment name should contain maximum 196 of the following allowed characters " + + "[a-zA-Z0-9_:./-] and should never finish with a semicolon." + + "In this case the Datadog SDK will not be initialised." + + internal const val MISSING_FEATURE_FOR_EVENT_RECEIVER = + "Cannot add event receiver for feature \"%s\", it is not registered." + internal const val MISSING_FEATURE_FOR_CONTEXT_UPDATE_LISTENER = + "Cannot add event listener for feature \"%s\", it is not registered." + internal const val EVENT_RECEIVER_ALREADY_EXISTS = + "Feature \"%s\" already has event receiver registered, overwriting it." + + internal const val NO_NEED_TO_WRITE_LAST_VIEW_EVENT = + "No need to write last RUM view event: NDK" + + " crash reports feature is not enabled and API is below 30." + + internal const val CONTEXT_UPDATE_LISTENER_ALREADY_REGISTERED = + "SDK core already has \"%s\" listener registered." + + internal val CONFIGURATION_TELEMETRY_DELAY_MS = TimeUnit.SECONDS.toMillis(5) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/FeatureContextProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/FeatureContextProvider.kt new file mode 100644 index 0000000000..9d25782972 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/FeatureContextProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +internal fun interface FeatureContextProvider { + /** + * Returns **frozen** snapshot of the feature context which is not mutated over the time. + */ + fun getFeatureContext(featureName: String): Map +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/HashGenerator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/HashGenerator.kt new file mode 100644 index 0000000000..0ec51adcdb --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/HashGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +internal interface HashGenerator { + fun generate(input: String): String? +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpContextProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpContextProvider.kt new file mode 100644 index 0000000000..baa2fad01e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpContextProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.DatadogSite +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.DeviceInfo +import com.datadog.android.api.context.DeviceType +import com.datadog.android.api.context.LocaleInfo +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.ProcessInfo +import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.privacy.TrackingConsent + +internal class NoOpContextProvider : ContextProvider { + // TODO RUM-3784 this one is quite ugly. Should return type be nullable? + override fun getContext(withFeatureContexts: Set) = DatadogContext( + site = DatadogSite.US1, + clientToken = "", + service = "", + env = "", + version = "", + variant = "", + source = "", + sdkVersion = "", + time = TimeInfo( + deviceTimeNs = 0L, + serverTimeNs = 0L, + serverTimeOffsetMs = 0L, + serverTimeOffsetNs = 0L + ), + processInfo = ProcessInfo(isMainProcess = true), + networkInfo = NetworkInfo( + connectivity = NetworkInfo.Connectivity.NETWORK_OTHER, + carrierName = null, + carrierId = null, + upKbps = null, + downKbps = null, + strength = null, + cellularTechnology = null + ), + deviceInfo = DeviceInfo( + deviceName = "", + deviceBrand = "", + deviceModel = "", + deviceType = DeviceType.OTHER, + deviceBuildId = "", + osName = "", + osMajorVersion = "", + osVersion = "", + architecture = "", + numberOfDisplays = null, + localeInfo = LocaleInfo( + locales = emptyList(), + currentLocale = "", + timeZone = "" + ) + ), + userInfo = UserInfo(null, null, null, null, emptyMap()), + accountInfo = null, + trackingConsent = TrackingConsent.NOT_GRANTED, + appBuildId = null, + featuresContext = emptyMap() + ) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt new file mode 100644 index 0000000000..bcbf20c2f7 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureContextUpdateReceiver +import com.datadog.android.api.feature.FeatureEventReceiver +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.internal.logger.SdkInternalLogger +import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver +import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.privacy.TrackingConsent +import com.google.gson.JsonObject +import java.io.File +import java.util.UUID +import java.util.concurrent.Callable +import java.util.concurrent.Delayed +import java.util.concurrent.ExecutionException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +/** + * A no-op implementation of [SdkCore]. + */ +internal object NoOpInternalSdkCore : InternalSdkCore { + + override val name: String = "no-op" + + override val time: TimeInfo = with(System.currentTimeMillis()) { + TimeInfo( + deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(this), + serverTimeNs = TimeUnit.MILLISECONDS.toNanos(this), + serverTimeOffsetNs = 0L, + serverTimeOffsetMs = 0L + ) + } + + override val service: String + get() = "" + + override val internalLogger: InternalLogger + get() = SdkInternalLogger(this) + + // region InternalSdkCore + + override val networkInfo: NetworkInfo + get() = NetworkInfo(NetworkInfo.Connectivity.NETWORK_OTHER) + override val trackingConsent: TrackingConsent + get() = TrackingConsent.NOT_GRANTED + override val rootStorageDir: File? + get() = null + override val isDeveloperModeEnabled: Boolean + get() = false + override val firstPartyHostResolver: FirstPartyHostHeaderTypeResolver + get() = DefaultFirstPartyHostHeaderTypeResolver(emptyMap()) + override val lastViewEvent: JsonObject? + get() = null + override val lastFatalAnrSent: Long? + get() = null + override val appStartTimeNs: Long + get() = 0 + + // endregion + + // region SdkCore + + override fun setTrackingConsent(consent: TrackingConsent) = Unit + + override fun setUserInfo( + id: String, + name: String?, + email: String?, + extraInfo: Map + ) = Unit + + override fun addUserProperties(extraInfo: Map) = Unit + + override fun clearUserInfo() = Unit + + override fun clearAllData() = Unit + + override fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) = Unit + + override fun addAccountExtraInfo(extraInfo: Map) = Unit + + override fun clearAccountInfo() = Unit + + override fun isCoreActive(): Boolean = false + + // endregion + + // region FeatureSdkCore + + override fun registerFeature(feature: Feature) = Unit + + override fun getFeature(featureName: String): FeatureScope? = null + + override fun updateFeatureContext( + featureName: String, + useContextThread: Boolean, + updateCallback: (MutableMap) -> Unit + ) = Unit + + override fun getFeatureContext(featureName: String, useContextThread: Boolean): Map = emptyMap() + + override fun setEventReceiver(featureName: String, `receiver`: FeatureEventReceiver) = Unit + + override fun removeEventReceiver(featureName: String) = Unit + + override fun setContextUpdateReceiver( + listener: FeatureContextUpdateReceiver + ) = Unit + + override fun removeContextUpdateReceiver( + listener: FeatureContextUpdateReceiver + ) = Unit + + override fun createSingleThreadExecutorService(executorContext: String): ExecutorService { + return NoOpExecutorService() + } + + override fun createScheduledExecutorService(executorContext: String): ScheduledExecutorService { + return NoOpScheduledExecutorService() + } + + override fun setAnonymousId(anonymousId: UUID?) = Unit + + // endregion + + // region InternalSdkCore + + override fun writeLastViewEvent(data: ByteArray) = Unit + + override fun deleteLastViewEvent() = Unit + + override fun writeLastFatalAnrSent(anrTimestamp: Long) = Unit + + override fun getPersistenceExecutorService(): ExecutorService = NoOpExecutorService() + + override fun getAllFeatures(): List = emptyList() + + override fun getDatadogContext(withFeatureContexts: Set): DatadogContext? = null + + // endregion + + class NoOpExecutorService : ExecutorService { + override fun execute(command: Runnable?) = Unit + + override fun shutdown() = Unit + + override fun shutdownNow(): MutableList = mutableListOf() + + override fun isShutdown(): Boolean = true + + override fun isTerminated(): Boolean = true + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean = true + + override fun submit(task: Callable?): Future? = null + + override fun submit(task: Runnable?, result: T): Future? = null + + override fun submit(task: Runnable?): Future<*>? = null + + override fun invokeAll( + tasks: MutableCollection>? + ): MutableList> = mutableListOf() + + override fun invokeAll( + tasks: MutableCollection>?, + timeout: Long, + unit: TimeUnit? + ): MutableList> = mutableListOf() + + override fun invokeAny(tasks: MutableCollection>?): T? = null + + override fun invokeAny( + tasks: MutableCollection>?, + timeout: Long, + unit: TimeUnit? + ): T? = null + } + + class NoOpScheduledExecutorService : ScheduledExecutorService { + override fun execute(command: Runnable?) = Unit + + override fun shutdown() = Unit + + override fun shutdownNow(): MutableList = mutableListOf() + + override fun isShutdown(): Boolean = true + + override fun isTerminated(): Boolean = true + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean = true + + override fun submit(task: Callable?): Future? = null + + override fun submit(task: Runnable?, result: T): Future? = null + + override fun submit(task: Runnable?): Future<*>? = null + + override fun invokeAll( + tasks: MutableCollection>? + ): MutableList> = mutableListOf() + + override fun invokeAll( + tasks: MutableCollection>?, + timeout: Long, + unit: TimeUnit? + ): MutableList> = mutableListOf() + + override fun invokeAny(tasks: MutableCollection>?): T? = null + + override fun invokeAny( + tasks: MutableCollection>?, + timeout: Long, + unit: TimeUnit? + ): T? = null + + override fun schedule(callable: Callable?, delay: Long, unit: TimeUnit?): ScheduledFuture { + return NoOpScheduledFuture() + } + + override fun schedule(command: Runnable?, delay: Long, unit: TimeUnit?): ScheduledFuture<*> { + return NoOpScheduledFuture() + } + + override fun scheduleAtFixedRate( + command: Runnable?, + initialDelay: Long, + period: Long, + unit: TimeUnit? + ): ScheduledFuture<*> { + return NoOpScheduledFuture() + } + + override fun scheduleWithFixedDelay( + command: Runnable?, + initialDelay: Long, + delay: Long, + unit: TimeUnit? + ): ScheduledFuture<*> { + return NoOpScheduledFuture() + } + } + + class NoOpScheduledFuture : ScheduledFuture { + override fun compareTo(other: Delayed?): Int { + return 0 + } + + override fun getDelay(unit: TimeUnit?): Long { + return 0L + } + + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + return false + } + + override fun isCancelled(): Boolean { + return false + } + + override fun isDone(): Boolean { + return false + } + + override fun get(): O { + throw ExecutionException("Unsupported", UnsupportedOperationException()) + } + + override fun get(timeout: Long, unit: TimeUnit?): O { + throw ExecutionException("Unsupported", UnsupportedOperationException()) + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkCoreRegistry.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkCoreRegistry.kt new file mode 100644 index 0000000000..eff7591ae1 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkCoreRegistry.kt @@ -0,0 +1,74 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore + +/** + * A Registry for all [SdkCore] instances, allowing customers to retrieve the one + * they want from anywhere in the code. + */ +internal class SdkCoreRegistry( + private val internalLogger: InternalLogger +) { + + private val instances = mutableMapOf() + + // region SdkCoreRegistry + + /** + * Register an instance of [SdkCore] with the given name. + * @param name the name for the given instance + * @param sdkCore the [SdkCore] instance + */ + fun register(name: String?, sdkCore: SdkCore) { + val key = name ?: DEFAULT_INSTANCE_NAME + if (instances.containsKey(key)) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { "An SdkCode with name $key has already been registered." } + ) + } else { + instances[key] = sdkCore + } + } + + /** + * Unregisters the instance for the given name. + * @param name the name of the instance to unregister + * @return the [SdkCore] instance if it exists, or null + */ + fun unregister(name: String? = null): SdkCore? { + val key = name ?: DEFAULT_INSTANCE_NAME + return instances.remove(key) + } + + /** + * Returns the instance for the given name. + * @param name the name of the instance to get + * @return the [SdkCore] instance if it exists, or null + */ + fun getInstance(name: String? = null): SdkCore? { + val key = name ?: DEFAULT_INSTANCE_NAME + return instances[key] + } + + /** + * Clears all registered instances. + */ + fun clear() { + instances.clear() + } + + // endregion + + companion object { + const val DEFAULT_INSTANCE_NAME = "_dd.sdk_core.default" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt new file mode 100644 index 0000000000..73f46f8d11 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt @@ -0,0 +1,472 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import android.app.Application +import android.content.Context +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureEventReceiver +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.StorageBackedFeature +import com.datadog.android.api.net.RequestFactory +import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import com.datadog.android.core.internal.data.upload.DataFlusher +import com.datadog.android.core.internal.data.upload.DataOkHttpUploader +import com.datadog.android.core.internal.data.upload.DataUploadScheduler +import com.datadog.android.core.internal.data.upload.DataUploader +import com.datadog.android.core.internal.data.upload.DefaultUploadSchedulerStrategy +import com.datadog.android.core.internal.data.upload.NoOpDataUploader +import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler +import com.datadog.android.core.internal.data.upload.UploadScheduler +import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor +import com.datadog.android.core.internal.metrics.BatchMetricsDispatcher +import com.datadog.android.core.internal.metrics.MetricsDispatcher +import com.datadog.android.core.internal.metrics.NoOpMetricsDispatcher +import com.datadog.android.core.internal.persistence.AbstractStorage +import com.datadog.android.core.internal.persistence.ConsentAwareStorage +import com.datadog.android.core.internal.persistence.NoOpStorage +import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileWriter +import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator +import com.datadog.android.core.internal.persistence.file.advanced.FeatureFileOrchestrator +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.core.internal.utils.getSafe +import com.datadog.android.core.internal.utils.submitSafe +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.internal.profiler.BenchmarkSdkUploads +import com.datadog.android.internal.profiler.GlobalBenchmark +import com.datadog.android.privacy.TrackingConsentProviderCallback +import com.datadog.android.security.Encryption +import java.util.Locale +import java.util.concurrent.Callable +import java.util.concurrent.Future +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock + +@Suppress("TooManyFunctions") +internal class SdkFeature( + internal val coreFeature: CoreFeature, + internal val contextProvider: ContextProvider, + internal val wrappedFeature: Feature, + internal val internalLogger: InternalLogger, + private val benchmarkSdkUploads: BenchmarkSdkUploads = GlobalBenchmark.getBenchmarkSdkUploads() +) : FeatureScope { + + override var dataStore: DataStoreHandler = NoOpDataStoreHandler() + + internal val initialized = AtomicBoolean(false) + internal val eventReceiver = AtomicReference(null) + internal var storage: Storage = NoOpStorage() + internal var uploader: DataUploader = NoOpDataUploader() + internal var uploadScheduler: UploadScheduler = NoOpUploadScheduler() + internal var fileOrchestrator: FileOrchestrator = NoOpFileOrchestrator() + internal var metricsDispatcher: MetricsDispatcher = NoOpMetricsDispatcher() + internal var processLifecycleMonitor: ProcessLifecycleMonitor? = null + internal val featureContextLock: ReadWriteLock = ReentrantReadWriteLock() + internal val featureContext: MutableMap = mutableMapOf() + + // region SdkFeature + + fun initialize(context: Context, instanceId: String) { + if (initialized.get()) { + return + } + + if (wrappedFeature is StorageBackedFeature) { + val uploadFrequency = coreFeature.uploadFrequency + val batchProcessingLevel = coreFeature.batchProcessingLevel + + val dataUploadConfiguration = DataUploadConfiguration( + uploadFrequency, + batchProcessingLevel.maxBatchesPerUploadJob + ) + val uploadSchedulerStrategy = coreFeature.customUploadSchedulerStrategy + ?: DefaultUploadSchedulerStrategy(dataUploadConfiguration) + storage = prepareStorage( + dataUploadConfiguration, + wrappedFeature, + context, + instanceId, + coreFeature.persistenceStrategyFactory + ) + + wrappedFeature.onInitialize(context) + + setupUploader(wrappedFeature, uploadSchedulerStrategy, dataUploadConfiguration.maxBatchesPerUploadJob) + } else { + wrappedFeature.onInitialize(context) + } + + if (wrappedFeature is TrackingConsentProviderCallback) { + coreFeature.trackingConsentProvider.registerCallback(wrappedFeature) + } + + prepareDataStoreHandler( + encryption = coreFeature.localDataEncryption + ) + + createBatchCountBenchmark() + + initialized.set(true) + + uploadScheduler.startScheduling() + } + + fun isInitialized(): Boolean { + return initialized.get() + } + + @AnyThread + fun clearAllData() { + storage.dropAll() + dataStore.clearAllData() + } + + fun stop() { + if (initialized.get()) { + wrappedFeature.onStop() + + if (wrappedFeature is TrackingConsentProviderCallback) { + coreFeature.trackingConsentProvider.unregisterCallback(wrappedFeature) + } + uploadScheduler.stopScheduling() + uploadScheduler = NoOpUploadScheduler() + storage = NoOpStorage() + dataStore = NoOpDataStoreHandler() + uploader = NoOpDataUploader() + fileOrchestrator = NoOpFileOrchestrator() + metricsDispatcher = NoOpMetricsDispatcher() + (coreFeature.contextRef.get() as? Application) + ?.unregisterActivityLifecycleCallbacks(processLifecycleMonitor) + processLifecycleMonitor = null + featureContext.clear() + initialized.set(false) + } + } + + // endregion + + // region FeatureScope + + override fun withWriteContext( + withFeatureContexts: Set, + callback: (DatadogContext, EventWriteScope) -> Unit + ) { + coreFeature.contextExecutorService + .executeSafe("withWriteContext-${wrappedFeature.name}", internalLogger) { + if (coreFeature.initialized.get() == false) return@executeSafe + val context = contextProvider.getContext(withFeatureContexts) + val eventBatchWriteScope = storage.getEventWriteScope(context) + callback(context, eventBatchWriteScope) + } + } + + override fun withContext( + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext) -> Unit + ) { + coreFeature.contextExecutorService + .executeSafe("withContext-${wrappedFeature.name}", internalLogger) { + if (coreFeature.initialized.get() == false) return@executeSafe + val context = contextProvider.getContext(withFeatureContexts) + callback(context) + } + } + + internal fun getContextFuture(withFeatureContexts: Set): Future? { + return coreFeature.contextExecutorService.submitSafe( + "getContextFuture-${wrappedFeature.name}", + internalLogger, + Callable { + if (coreFeature.initialized.get()) { + contextProvider.getContext(withFeatureContexts) + } else { + null + } + } + ) + } + + override fun getWriteContextSync( + withFeatureContexts: Set + ): Pair? { + val operationName = "getWriteContextSync-${wrappedFeature.name}" + return coreFeature.contextExecutorService + .submitSafe( + operationName, + internalLogger, + Callable { + if (coreFeature.initialized.get() == false) return@Callable null + val context = contextProvider.getContext(withFeatureContexts) + val eventBatchWriteScope = storage.getEventWriteScope(context) + context to eventBatchWriteScope + } + ) + .getSafe(operationName, internalLogger) + } + + override fun sendEvent(event: Any) { + val receiver = eventReceiver.get() + if (receiver == null) { + internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + { NO_EVENT_RECEIVER.format(Locale.US, wrappedFeature.name) } + ) + } else { + receiver.onReceive(event) + } + } + + // Allow unchecked cast here. Anyway if there is a mismatch and crash happens, it will be + // caught during our tests + @Suppress("UNCHECKED_CAST") + override fun unwrap(): T = wrappedFeature as T + + // endregion + + // region Internal + + private fun createBatchCountBenchmark() { + val tags = mapOf( + TRACK_NAME to wrappedFeature.name + ) + + @Suppress("ThreadSafety") // called in worker thread context + benchmarkSdkUploads + .getMeter(METER_NAME) + .createObservableGauge( + metricName = BATCH_COUNT_METRIC_NAME, + tags = tags, + callback = { fileOrchestrator.getFlushableFiles().size.toDouble() } + ) + } + + private fun setupMetricsDispatcher( + dataUploadConfiguration: DataUploadConfiguration?, + filePersistenceConfig: FilePersistenceConfig, + context: Context + ) { + metricsDispatcher = BatchMetricsDispatcher( + wrappedFeature.name, + dataUploadConfiguration, + filePersistenceConfig, + internalLogger, + coreFeature.timeProvider + ).apply { + if (context is Application) { + processLifecycleMonitor = ProcessLifecycleMonitor(this) + context.registerActivityLifecycleCallbacks( + processLifecycleMonitor + ) + } + } + } + + private fun setupUploader( + feature: StorageBackedFeature, + uploadSchedulerStrategy: UploadSchedulerStrategy, + maxBatchesPerJob: Int + ) { + uploadScheduler = if (coreFeature.isMainProcess) { + uploader = createUploader(feature.requestFactory) + DataUploadScheduler( + feature.name, + storage, + uploader, + contextProvider, + coreFeature.networkInfoProvider, + coreFeature.systemInfoProvider, + uploadSchedulerStrategy, + maxBatchesPerJob, + coreFeature.uploadExecutorService, + internalLogger + ) + } else { + NoOpUploadScheduler() + } + } + + // region Feature setup + + private fun prepareStorage( + dataUploadConfiguration: DataUploadConfiguration?, + wrappedFeature: StorageBackedFeature, + context: Context, + instanceId: String, + persistenceStrategyFactory: PersistenceStrategy.Factory? + ): Storage { + val storageConfiguration = wrappedFeature.storageConfiguration + return if (persistenceStrategyFactory == null) { + val recentDelayMs = coreFeature.batchSize.windowDurationMs + val filePersistenceConfig = coreFeature.buildFilePersistenceConfig().copy( + maxBatchSize = storageConfiguration.maxBatchSize, + maxItemSize = storageConfiguration.maxItemSize, + maxItemsPerBatch = storageConfiguration.maxItemsPerBatch, + oldFileThreshold = storageConfiguration.oldBatchThreshold, + recentDelayMs = recentDelayMs + ) + setupMetricsDispatcher(dataUploadConfiguration, filePersistenceConfig, context) + + createFileStorage(wrappedFeature.name, filePersistenceConfig) + } else { + createCustomStorage(instanceId, wrappedFeature.name, storageConfiguration, persistenceStrategyFactory) + } + } + + private fun createCustomStorage( + instanceId: String, + featureName: String, + storageConfiguration: FeatureStorageConfiguration, + persistenceStrategyFactory: PersistenceStrategy.Factory + ): Storage { + return AbstractStorage( + instanceId, + featureName, + persistenceStrategyFactory, + coreFeature.persistenceExecutorService, + internalLogger, + storageConfiguration, + coreFeature.trackingConsentProvider + ) + } + + private fun createFileStorage( + featureName: String, + filePersistenceConfig: FilePersistenceConfig + ): Storage { + val fileOrchestrator = FeatureFileOrchestrator( + consentProvider = coreFeature.trackingConsentProvider, + storageDir = coreFeature.storageDir, + featureName = featureName, + executorService = coreFeature.persistenceExecutorService, + filePersistenceConfig = filePersistenceConfig, + internalLogger = internalLogger, + metricsDispatcher = metricsDispatcher + ) + this.fileOrchestrator = fileOrchestrator + + return ConsentAwareStorage( + executorService = coreFeature.persistenceExecutorService, + grantedOrchestrator = fileOrchestrator.grantedOrchestrator, + pendingOrchestrator = fileOrchestrator.pendingOrchestrator, + batchEventsReaderWriter = BatchFileReaderWriter.create( + internalLogger = internalLogger, + encryption = coreFeature.localDataEncryption + ), + batchMetadataReaderWriter = FileReaderWriter.create( + internalLogger = internalLogger, + encryption = coreFeature.localDataEncryption + ), + fileMover = FileMover(internalLogger), + internalLogger = internalLogger, + filePersistenceConfig = filePersistenceConfig, + metricsDispatcher = metricsDispatcher, + featureName + ) + } + + private fun createUploader(requestFactory: RequestFactory): DataUploader { + return DataOkHttpUploader( + requestFactory = requestFactory, + internalLogger = internalLogger, + callFactory = coreFeature.callFactory, + sdkVersion = coreFeature.sdkVersion, + androidInfoProvider = coreFeature.androidInfoProvider, + executionTimer = GlobalBenchmark.createExecutionTimer( + track = wrappedFeature.name + ) + ) + } + + private fun prepareDataStoreHandler( + encryption: Encryption? + ) { + val fileReaderWriter = FileReaderWriter.create( + internalLogger, + encryption + ) + + val dataStoreFileHelper = DataStoreFileHelper(internalLogger) + val featureName = wrappedFeature.name + val storageDir = coreFeature.storageDir + + val tlvBlockFileReader = TLVBlockFileReader( + internalLogger = internalLogger, + fileReaderWriter = fileReaderWriter + ) + + val dataStoreFileReader = DatastoreFileReader( + dataStoreFileHelper = dataStoreFileHelper, + featureName = featureName, + internalLogger = internalLogger, + storageDir = storageDir, + tlvBlockFileReader = tlvBlockFileReader + ) + + val dataStoreFileWriter = DatastoreFileWriter( + dataStoreFileHelper = dataStoreFileHelper, + featureName = featureName, + fileReaderWriter = fileReaderWriter, + internalLogger = internalLogger, + storageDir = storageDir + ) + + dataStore = DataStoreFileHandler( + executorService = coreFeature.persistenceExecutorService, + internalLogger = internalLogger, + dataStoreFileReader = dataStoreFileReader, + datastoreFileWriter = dataStoreFileWriter + ) + } + + // endregion + + // Used for nightly tests only + @WorkerThread + internal fun flushStoredData() { + val flusher = DataFlusher( + contextProvider, + fileOrchestrator, + BatchFileReaderWriter.create(internalLogger, coreFeature.localDataEncryption), + FileReaderWriter.create(internalLogger, coreFeature.localDataEncryption), + FileMover(internalLogger), + internalLogger + ) + flusher.flush(uploader) + } + + // endregion + + companion object { + const val NO_EVENT_RECEIVER = + "Feature \"%s\" has no event receiver registered, ignoring event." + internal const val TRACK_NAME = "track" + internal const val METER_NAME = "dd-sdk-android" + internal const val BATCH_COUNT_METRIC_NAME = "android.benchmark.batch_count" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/Sha256HashGenerator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/Sha256HashGenerator.kt new file mode 100644 index 0000000000..8b44e391d8 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/Sha256HashGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.utils.unboundInternalLogger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.Locale + +internal class Sha256HashGenerator : HashGenerator { + override fun generate(input: String): String? { + return try { + val messageDigest = MessageDigest.getInstance("SHA-256") + messageDigest.update(input.toByteArray(Charsets.UTF_8)) + + val hashBytes = messageDigest.digest() + + hashBytes.joinToString(separator = "") { "%02x".format(Locale.US, it) } + } catch (e: NoSuchAlgorithmException) { + unboundInternalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { SHA_256_HASH_GENERATION_ERROR }, + e + ) + null + } + } + + companion object { + const val SHA_256_HASH_GENERATION_ERROR = "Cannot generate SHA-256 hash." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/AccountInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/AccountInfoProvider.kt new file mode 100644 index 0000000000..0701769f3f --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/AccountInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.account + +import com.datadog.android.api.context.AccountInfo +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface AccountInfoProvider { + + fun getAccountInfo(): AccountInfo? +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/DatadogAccountInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/DatadogAccountInfoProvider.kt new file mode 100644 index 0000000000..593fd543fd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/DatadogAccountInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.account + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.AccountInfo + +internal class DatadogAccountInfoProvider( + private val internalLogger: InternalLogger +) : MutableAccountInfoProvider { + + @Volatile + private var internalAccountInfo: AccountInfo? = null + + override fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) { + internalAccountInfo = internalAccountInfo?.copy( + id = id, + name = name, + extraInfo = extraInfo.toMap() + ) ?: AccountInfo( + id = id, + name = name, + extraInfo = extraInfo.toMap() + ) + } + + override fun addExtraInfo(extraInfo: Map) { + internalAccountInfo?.let { + internalAccountInfo = it.copy( + extraInfo = it.extraInfo + extraInfo + ) + } ?: run { + internalLogger.log( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.USER, + messageBuilder = { MSG_ACCOUNT_NULL } + ) + } + } + + override fun clearAccountInfo() { + internalAccountInfo = null + } + + override fun getAccountInfo(): AccountInfo? { + return internalAccountInfo + } + + companion object { + internal const val MSG_ACCOUNT_NULL = + "Failed to add Account ExtraInfo because no Account Info exist yet. Please call `setAccountInfo` first." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/MutableAccountInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/MutableAccountInfoProvider.kt new file mode 100644 index 0000000000..21e14dfa46 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/account/MutableAccountInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.account + +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface MutableAccountInfoProvider : AccountInfoProvider { + + fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) + + fun addExtraInfo(extraInfo: Map) + + fun clearAccountInfo() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/configuration/DataUploadConfiguration.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/configuration/DataUploadConfiguration.kt new file mode 100644 index 0000000000..457f249941 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/configuration/DataUploadConfiguration.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.configuration + +import com.datadog.android.core.configuration.UploadFrequency + +internal data class DataUploadConfiguration( + internal val frequency: UploadFrequency, + internal val maxBatchesPerUploadJob: Int +) { + internal val minDelayMs = MIN_DELAY_FACTOR * frequency.baseStepMs + internal val maxDelayMs = MAX_DELAY_FACTOR * frequency.baseStepMs + internal val defaultDelayMs = DEFAULT_DELAY_FACTOR * frequency.baseStepMs + + companion object { + internal const val MIN_DELAY_FACTOR = 1 + internal const val MAX_DELAY_FACTOR = 10 + internal const val DEFAULT_DELAY_FACTOR = 5 + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/constraints/StringTransform.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/constraints/StringTransform.kt new file mode 100644 index 0000000000..29e0f48a73 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/constraints/StringTransform.kt @@ -0,0 +1,9 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.constraints + +internal typealias StringTransform = (String) -> String? diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/CurlInterceptor.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/CurlInterceptor.kt new file mode 100644 index 0000000000..91244a72db --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/CurlInterceptor.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import android.util.Log +import okhttp3.Interceptor +import okhttp3.MultipartBody +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okio.Buffer +import java.io.IOException +import java.nio.charset.Charset +import java.util.Locale +import kotlin.jvm.Throws + +/** + * This interceptor logs the request as a valid CURL command line. + */ +internal class CurlInterceptor( + private val printBody: Boolean = false, + private val output: (String) -> Unit = { Log.i("Curl", it) } +) : Interceptor { + + // region Interceptor + + /** + * Observes, modifies, or short-circuits requests going out and the responses coming back in. + */ + // let the proceed exception be handled by the caller + @Suppress("UnsafeThirdPartyFunctionCall") + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + val copy = request.newBuilder().build() + val curl: String = CurlBuilder(copy, printBody).toCommand() + output(curl) + + return chain.proceed(request) + } + + // endregion + + // region Internal + + class CurlBuilder( + val url: String, + val method: String, + val contentType: String? = null, + val requestBody: RequestBody? = null, + val headers: Map> = emptyMap(), + val printBody: Boolean + ) { + + constructor(request: Request, printBody: Boolean) : + this( + url = request.url.toString(), + method = request.method, + contentType = request.body?.contentType()?.toString(), + requestBody = request.body, + headers = request.headers.toMultimap(), + printBody = printBody + ) + + fun toCommand(): String = buildString { + append("curl").append(' ') + append(FORMAT_METHOD.format(Locale.US, method.uppercase(Locale.US))).append(' ') + headers.forEach { (key, values) -> + values.forEach { value -> + append(FORMAT_HEADER.format(Locale.US, key, value)).append(' ') + } + } + + if (contentType != null && !headers.containsKey(CONTENT_TYPE)) { + append(FORMAT_HEADER.format(Locale.US, CONTENT_TYPE, contentType)).append(' ') + } + + requestBody?.toParts()?.forEach { append(it).append(' ') } + append(FORMAT_URL.format(Locale.US, url)) + } + + private fun RequestBody.toParts(): List { + return if (this is MultipartBody) { + val requestCurlPart = mutableListOf() + this.parts.forEach { + it.headers?.toMultimap()?.forEach { (key, value) -> + requestCurlPart.add(FORMAT_HEADER.format(Locale.US, key, value)) + } + if (printBody) { + requestCurlPart.add(FORMAT_BODY.format(Locale.US, peekBody(it.body))) + } + } + requestCurlPart + } else { + if (printBody) { + listOf(FORMAT_BODY.format(Locale.US, peekBody(this))) + } else { + emptyList() + } + } + } + } + + // endregion + + companion object { + + private const val FORMAT_HEADER = "-H \"%1\$s:%2\$s\"" + private const val FORMAT_METHOD = "-X %1\$s" + private const val FORMAT_BODY = "-d '%1\$s'" + private const val FORMAT_URL = "\"%1\$s\"" + private const val CONTENT_TYPE = "Content-Type" + + private fun peekBody(body: RequestBody?): String? { + if (body == null) return null + + return try { + val sink = Buffer() + val charset: Charset = Charset.defaultCharset() + + body.writeTo(sink) + sink.readString(charset) + } catch (e: IOException) { + "Error while reading body: $e" + } catch (e: IllegalArgumentException) { + "Error while reading body: $e" + } + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataFlusher.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataFlusher.kt new file mode 100644 index 0000000000..0a8b91ebd8 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataFlusher.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.ContextProvider +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FileReader +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReader +import com.datadog.android.core.internal.persistence.file.existsSafe + +internal class DataFlusher( + internal val contextProvider: ContextProvider, + internal val fileOrchestrator: FileOrchestrator, + internal val fileReader: BatchFileReader, + internal val metadataFileReader: FileReader, + internal val fileMover: FileMover, + private val internalLogger: InternalLogger +) : Flusher { + + @WorkerThread + override fun flush(uploader: DataUploader) { + val context = contextProvider.getContext(withFeatureContexts = emptySet()) + + val toUploadFiles = fileOrchestrator.getFlushableFiles() + toUploadFiles.forEach { + val batch = fileReader.readData(it) + val metaFile = fileOrchestrator.getMetadataFile(it) + val meta = if (metaFile != null && metaFile.existsSafe(internalLogger)) { + metadataFileReader.readData(metaFile) + } else { + null + } + uploader.upload(context, batch, meta) + fileMover.delete(it) + if (metaFile?.existsSafe(internalLogger) == true) { + fileMover.delete(metaFile) + } + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploader.kt new file mode 100644 index 0000000000..9a4daa31f1 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import android.net.TrafficStats +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.net.RequestExecutionContext +import com.datadog.android.api.net.RequestFactory +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.BatchId +import com.datadog.android.core.internal.system.AndroidInfoProvider +import com.datadog.android.internal.profiler.ExecutionTimer +import com.datadog.android.internal.utils.safeGetThreadId +import okhttp3.Call +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import java.net.UnknownHostException +import java.util.Locale +import com.datadog.android.api.net.Request as DatadogRequest + +internal class DataOkHttpUploader( + val requestFactory: RequestFactory, + val internalLogger: InternalLogger, + val callFactory: Call.Factory, + val sdkVersion: String, + val androidInfoProvider: AndroidInfoProvider, + val executionTimer: ExecutionTimer +) : DataUploader { + + @Volatile + private var attempts: Int = 1 + + @Volatile + private var previousUploadStatus: UploadStatus? = null + + @Volatile + private var previousUploadedBatchId: BatchId? = null + + // region DataUploader + + @Suppress("TooGenericExceptionCaught", "ReturnCount") + override fun upload( + context: DatadogContext, + batch: List, + batchMeta: ByteArray?, + batchId: BatchId? + ): UploadStatus { + val executionContext = resolveExecutionContext(batchId) + val request = try { + requestFactory.create(context, executionContext, batch, batchMeta) + ?: return UploadStatus.RequestCreationError(null) + } catch (e: Exception) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { + "Unable to create the request, probably due to bad data format." + + " The batch will be dropped." + }, + e + ) + return UploadStatus.RequestCreationError(e) + } + + val uploadStatus = + executionTimer.measure { + try { + executeUploadRequest(request) + } catch (e: UnknownHostException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Unable to find host for site ${context.site}; we will retry later." }, + e + ) + UploadStatus.DNSError(e) + } catch (e: IOException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Unable to execute the request; we will retry later." }, + e + ) + UploadStatus.NetworkError(e) + } catch (e: Throwable) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Unable to execute the request; we will retry later." }, + e + ) + UploadStatus.UnknownException(throwable = e) + } + } + + uploadStatus.logStatus( + request.description, + request.body.size, + internalLogger, + attempts = executionContext.attemptNumber, + requestId = request.id + ) + previousUploadStatus = uploadStatus + return uploadStatus + } + + // endregion + + private val userAgent by lazy { + sanitizeHeaderValue(System.getProperty(SYSTEM_UA)) + .ifBlank { + "Datadog/$sdkVersion " + + "(Linux; U; Android ${androidInfoProvider.osVersion}; " + + "${androidInfoProvider.deviceModel} " + + "Build/${androidInfoProvider.deviceBuildId})" + } + } + + // region Internal + private fun resolveExecutionContext(batchID: BatchId?): RequestExecutionContext { + val previousResponseCode: Int? + if ((batchID != null && previousUploadedBatchId != null) && (previousUploadedBatchId == batchID)) { + attempts++ + previousResponseCode = previousUploadStatus?.code + } else { + attempts = 1 + previousResponseCode = null + } + previousUploadedBatchId = batchID + return RequestExecutionContext( + attemptNumber = attempts, + previousResponseCode = previousResponseCode + ) + } + + @Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block + private fun executeUploadRequest( + request: DatadogRequest + ): UploadStatus { + val apiKey = request.headers.entries + .firstOrNull { + it.key.equals(RequestFactory.HEADER_API_KEY, ignoreCase = true) + } + ?.value + if (apiKey != null && (apiKey.isEmpty() || !isValidHeaderValue(apiKey))) { + return UploadStatus.InvalidTokenError(UploadStatus.UNKNOWN_RESPONSE_CODE) + } + + val okHttpRequest = buildOkHttpRequest(request) + TrafficStats.setThreadStatsTag(Thread.currentThread().safeGetThreadId().toInt()) + val call = callFactory.newCall(okHttpRequest) + val response = call.execute() + response.close() + return responseCodeToUploadStatus(response.code, request) + } + + @Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block + private fun buildOkHttpRequest( + request: DatadogRequest + ): Request { + val mediaType = if (request.contentType == null) { + null + } else { + request.contentType.toMediaTypeOrNull() + } + val builder = Request.Builder() + .url(http://github.com/request.url) + .post(request.body.toRequestBody(mediaType)) + + for ((header, value) in request.headers) { + if (header.lowercase(Locale.US) == "user-agent") { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { WARNING_USER_AGENT_HEADER_RESERVED } + ) + continue + } + builder.addHeader(header, value) + } + + builder.addHeader(HEADER_USER_AGENT, userAgent) + + return builder.build() + } + + private fun sanitizeHeaderValue(value: String?): String { + return value?.filter { isValidHeaderValueChar(it) }.orEmpty() + } + + private fun isValidHeaderValue(value: String): Boolean { + return value.all { isValidHeaderValueChar(it) } + } + + private fun isValidHeaderValueChar(c: Char): Boolean { + return c == '\t' || c in '\u0020' until '\u007F' + } + + private fun responseCodeToUploadStatus( + code: Int, + request: DatadogRequest + ): UploadStatus { + return when (code) { + HTTP_ACCEPTED -> UploadStatus.Success(code) + + HTTP_UNAUTHORIZED, + HTTP_FORBIDDEN -> UploadStatus.InvalidTokenError(code) + + HTTP_CLIENT_TIMEOUT, + HTTP_TOO_MANY_REQUESTS -> UploadStatus.HttpClientRateLimiting(code) + + HTTP_BAD_REQUEST, + HTTP_ENTITY_TOO_LARGE -> UploadStatus.HttpClientError(code) + + HTTP_INTERNAL_ERROR, + HTTP_BAD_GATEWAY, + HTTP_UNAVAILABLE, + HTTP_GATEWAY_TIMEOUT, + HTTP_INSUFFICIENT_STORAGE -> UploadStatus.HttpServerError(code) + + else -> { + internalLogger.log( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { "Unexpected status code $code on upload request: ${request.description}" } + ) + UploadStatus.UnknownHttpError(code) + } + } + } + + companion object { + + const val HTTP_ACCEPTED = 202 + const val HTTP_BAD_REQUEST = 400 + const val HTTP_UNAUTHORIZED = 401 + const val HTTP_FORBIDDEN = 403 + const val HTTP_CLIENT_TIMEOUT = 408 + const val HTTP_ENTITY_TOO_LARGE = 413 + const val HTTP_TOO_MANY_REQUESTS = 429 + + const val HTTP_INTERNAL_ERROR = 500 + const val HTTP_BAD_GATEWAY = 502 + const val HTTP_UNAVAILABLE = 503 + const val HTTP_GATEWAY_TIMEOUT = 504 + const val HTTP_INSUFFICIENT_STORAGE = 507 + + const val SYSTEM_UA = "http.agent" + + const val WARNING_USER_AGENT_HEADER_RESERVED = + "Ignoring provided User-Agent header, because it is reserved." + const val HEADER_USER_AGENT = "User-Agent" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnable.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnable.kt new file mode 100644 index 0000000000..4804756daf --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnable.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.ContextProvider +import com.datadog.android.core.internal.metrics.BenchmarkUploads +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.net.info.NetworkInfoProvider +import com.datadog.android.core.internal.persistence.BatchId +import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.system.SystemInfoProvider +import com.datadog.android.core.internal.utils.scheduleSafe +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit + +internal class DataUploadRunnable( + private val featureName: String, + private val threadPoolExecutor: ScheduledThreadPoolExecutor, + private val storage: Storage, + private val dataUploader: DataUploader, + private val contextProvider: ContextProvider, + private val networkInfoProvider: NetworkInfoProvider, + private val systemInfoProvider: SystemInfoProvider, + internal val uploadSchedulerStrategy: UploadSchedulerStrategy, + internal val maxBatchesPerJob: Int, + private val internalLogger: InternalLogger, + private val benchmarkUploads: BenchmarkUploads = BenchmarkUploads() +) : UploadRunnable { + + // region Runnable + + @WorkerThread + override fun run() { + var uploadAttempts = 0 + var lastBatchUploadStatus: UploadStatus? = null + if (isNetworkAvailable() && isSystemReady()) { + val context = contextProvider.getContext(withFeatureContexts = emptySet()) + var batchConsumerAvailableAttempts = maxBatchesPerJob + do { + benchmarkUploads.incrementBenchmarkUploadsCount( + featureName = featureName + ) + batchConsumerAvailableAttempts-- + lastBatchUploadStatus = handleNextBatch(context) + if (lastBatchUploadStatus != null) { + uploadAttempts++ + } + } while ( + batchConsumerAvailableAttempts > 0 && lastBatchUploadStatus is UploadStatus.Success + ) + } + + val delayMs = uploadSchedulerStrategy.getMsDelayUntilNextUpload( + featureName, + uploadAttempts, + lastBatchUploadStatus?.code, + lastBatchUploadStatus?.throwable + ) + scheduleNextUpload(delayMs) + } + + // endregion + + // region Internal + + @WorkerThread + @Suppress("UnsafeThirdPartyFunctionCall") // called inside a dedicated executor + private fun handleNextBatch(context: DatadogContext): UploadStatus? { + var uploadStatus: UploadStatus? = null + val nextBatchData = storage.readNextBatch() + if (nextBatchData != null) { + uploadStatus = consumeBatch( + context, + nextBatchData.id, + nextBatchData.data, + nextBatchData.metadata + ) + } + return uploadStatus + } + + private fun isNetworkAvailable(): Boolean { + val networkInfo = networkInfoProvider.getLatestNetworkInfo() + return networkInfo.connectivity != NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED + } + + private fun isSystemReady(): Boolean { + val systemInfo = systemInfoProvider.getLatestSystemInfo() + val hasEnoughPower = systemInfo.batteryFullOrCharging || + systemInfo.onExternalPowerSource || + systemInfo.batteryLevel > LOW_BATTERY_THRESHOLD + return hasEnoughPower && !systemInfo.powerSaveMode + } + + private fun scheduleNextUpload(delayMs: Long) { + threadPoolExecutor.remove(this) + threadPoolExecutor.scheduleSafe( + "$featureName: data upload", + delayMs, + TimeUnit.MILLISECONDS, + internalLogger, + this + ) + } + + @WorkerThread + private fun consumeBatch( + context: DatadogContext, + batchId: BatchId, + batch: List, + batchMeta: ByteArray? + ): UploadStatus { + val status = dataUploader.upload(context, batch, batchMeta, batchId) + + if (status is UploadStatus.Success) { + val uploadedBytes = batch.sumOf { it.data.size } + benchmarkUploads.sendBenchmarkBytesUploaded( + featureName = featureName, + value = uploadedBytes.toLong() + ) + } + + val removalReason = if (status is UploadStatus.RequestCreationError) { + RemovalReason.Invalid + } else { + RemovalReason.IntakeCode(status.code) + } + storage.confirmBatchRead(batchId, removalReason, deleteBatch = !status.shouldRetry) + return status + } + + // endregion + + companion object { + internal const val LOW_BATTERY_THRESHOLD = 10 + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadScheduler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadScheduler.kt new file mode 100644 index 0000000000..4b2fc70fdd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadScheduler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.ContextProvider +import com.datadog.android.core.internal.net.info.NetworkInfoProvider +import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.system.SystemInfoProvider +import com.datadog.android.core.internal.utils.executeSafe +import java.util.concurrent.ScheduledThreadPoolExecutor + +internal class DataUploadScheduler( + private val featureName: String, + storage: Storage, + dataUploader: DataUploader, + contextProvider: ContextProvider, + networkInfoProvider: NetworkInfoProvider, + systemInfoProvider: SystemInfoProvider, + uploadSchedulerStrategy: UploadSchedulerStrategy, + maxBatchesPerJob: Int, + private val scheduledThreadPoolExecutor: ScheduledThreadPoolExecutor, + private val internalLogger: InternalLogger +) : UploadScheduler { + + internal val runnable = DataUploadRunnable( + featureName = featureName, + threadPoolExecutor = scheduledThreadPoolExecutor, + storage = storage, + dataUploader = dataUploader, + contextProvider = contextProvider, + networkInfoProvider = networkInfoProvider, + systemInfoProvider = systemInfoProvider, + uploadSchedulerStrategy = uploadSchedulerStrategy, + maxBatchesPerJob = maxBatchesPerJob, + internalLogger = internalLogger + ) + + override fun startScheduling() { + scheduledThreadPoolExecutor.executeSafe( + "$featureName: data upload", + internalLogger, + runnable + ) + } + + override fun stopScheduling() { + scheduledThreadPoolExecutor.remove(runnable) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploader.kt new file mode 100644 index 0000000000..9ec1684d78 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.BatchId + +internal interface DataUploader { + fun upload( + context: DatadogContext, + batch: List, + batchMeta: ByteArray?, + batchId: BatchId? = null + ): UploadStatus +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategy.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategy.kt new file mode 100644 index 0000000000..71be80e9dc --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategy.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import com.datadog.android.core.internal.data.upload.DataOkHttpUploader.Companion.HTTP_ACCEPTED +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import kotlin.math.min +import kotlin.math.roundToLong + +internal class DefaultUploadSchedulerStrategy( + internal val uploadConfiguration: DataUploadConfiguration +) : UploadSchedulerStrategy { + + private val currentDelays = ConcurrentHashMap() + + // region UploadSchedulerStrategy + + override fun getMsDelayUntilNextUpload( + featureName: String, + uploadAttempts: Int, + lastStatusCode: Int?, + throwable: Throwable? + ): Long { + val previousDelay = currentDelays.getOrPut(featureName) { uploadConfiguration.defaultDelayMs } + val updatedDelay = if (uploadAttempts > 0 && throwable == null && lastStatusCode == HTTP_ACCEPTED) { + uploadConfiguration.minDelayMs + } else { + increaseInterval(previousDelay, throwable) + } + currentDelays[featureName] = updatedDelay + return updatedDelay + } + + // endregion + + // region Internal + + private fun increaseInterval(previousDelay: Long, throwable: Throwable?): Long { + @Suppress("UnsafeThirdPartyFunctionCall") // not a NaN + val newDelayMs = (previousDelay * INCREASE_PERCENT).roundToLong() + + return if (throwable is IOException) { + // An IOException can mean a DNS error, or network connection loss + // Those aren't likely to be a fluke or flakiness, so we use a longer delay to avoid infinite looping + // and prevent battery draining + NETWORK_ERROR_DELAY_MS + } else { + min(uploadConfiguration.maxDelayMs, newDelayMs) + } + } + + // endregion + + companion object { + internal const val INCREASE_PERCENT = 1.10 + internal val NETWORK_ERROR_DELAY_MS = TimeUnit.MINUTES.toMillis(1) // 1 minute delay + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/Flusher.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/Flusher.kt new file mode 100644 index 0000000000..57f6de1213 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/Flusher.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import androidx.annotation.WorkerThread +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface Flusher { + + @WorkerThread + fun flush(uploader: DataUploader) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/GzipRequestInterceptor.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/GzipRequestInterceptor.kt new file mode 100644 index 0000000000..3a28e21eb6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/GzipRequestInterceptor.kt @@ -0,0 +1,97 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okio.BufferedSink +import okio.GzipSink +import okio.buffer +import java.io.IOException +import kotlin.jvm.Throws + +/** + * This interceptor compresses the HTTP request body. + * + * This class uses the [GzipSink] to compress the body content. + */ +internal class GzipRequestInterceptor(private val internalLogger: InternalLogger) : Interceptor { + + // region Interceptor + + /** + * Observes, modifies, or short-circuits requests going out and the responses coming back in. + */ + // let the proceed exception be handled by the caller + @Suppress("UnsafeThirdPartyFunctionCall", "TooGenericExceptionCaught") + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest: Request = chain.request() + val body = originalRequest.body + + return if (body == null || + originalRequest.header(HEADER_ENCODING) != null || + body is MultipartBody + ) { + chain.proceed(originalRequest) + } else { + val compressedRequest = try { + originalRequest.newBuilder() + .header(HEADER_ENCODING, ENCODING_GZIP) + .method(originalRequest.method, gzip(body)) + .build() + } catch (e: Exception) { + internalLogger.log( + InternalLogger.Level.WARN, + targets = listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { "Unable to gzip request body" }, + e + ) + originalRequest + } + chain.proceed(compressedRequest) + } + } + + // endregion + + // region Internal + + private fun gzip(body: RequestBody): RequestBody? { + return object : RequestBody() { + override fun contentType(): MediaType? { + return body.contentType() + } + + override fun contentLength(): Long { + return -1 // We don't know the compressed length in advance! + } + + @Suppress("UnsafeThirdPartyFunctionCall") // write to is expected to throw IOExceptions + override fun writeTo(sink: BufferedSink) { + val gzipSink: BufferedSink = GzipSink(sink).buffer() + body.writeTo(gzipSink) + gzipSink.close() + } + } + } + + // endregion + + companion object { + private const val HEADER_ENCODING = "Content-Encoding" + private const val ENCODING_GZIP = "gzip" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/NoOpDataUploader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/NoOpDataUploader.kt new file mode 100644 index 0000000000..6795f4b0e1 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/NoOpDataUploader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.BatchId + +internal class NoOpDataUploader : DataUploader { + override fun upload( + context: DatadogContext, + batch: List, + batchMeta: ByteArray?, + batchId: BatchId? + ): UploadStatus { + return UploadStatus.UnknownStatus + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/RotatingDnsResolver.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/RotatingDnsResolver.kt new file mode 100644 index 0000000000..a176a6bdb6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/RotatingDnsResolver.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import okhttp3.Dns +import java.net.InetAddress +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.nanoseconds + +internal class RotatingDnsResolver( + private val delegate: Dns = Dns.SYSTEM, + private val ttl: Duration = TTL_30_MIN +) : Dns { + + data class ResolvedHost( + val hostname: String, + val addresses: MutableList + ) { + private val resolutionTimestamp: Long = System.nanoTime() + + fun getAge(): Duration { + return (System.nanoTime() - resolutionTimestamp).nanoseconds + } + + fun rotate() { + synchronized(addresses) { + val first = addresses.removeFirstOrNull() + if (first != null) { + addresses.add(first) + } + } + } + } + + private val knownHosts = mutableMapOf() + + // region Dns + + override fun lookup(hostname: String): List { + val knownHost = knownHosts[hostname] + + return if (knownHost != null && isValid(knownHost)) { + knownHost.rotate() + safeCopy(knownHost.addresses) + } else { + @Suppress("UnsafeThirdPartyFunctionCall") // handled by caller + val result = delegate.lookup(hostname) + knownHosts[hostname] = ResolvedHost(hostname, result.toMutableList()) + safeCopy(result) + } + } + + // endregion + + // region Internal + + private fun safeCopy(list: List): List { + return synchronized(list) { + list.toList() + } + } + + private fun isValid(knownHost: ResolvedHost): Boolean { + return knownHost.getAge() < ttl && knownHost.addresses.isNotEmpty() + } + + // endregion + + companion object { + val TTL_30_MIN: Duration = 30.minutes + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadRunnable.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadRunnable.kt similarity index 100% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadRunnable.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadRunnable.kt diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadScheduler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadScheduler.kt similarity index 100% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadScheduler.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadScheduler.kt diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadStatus.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadStatus.kt new file mode 100644 index 0000000000..f79127c6cb --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadStatus.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import java.util.Locale + +internal sealed class UploadStatus( + val shouldRetry: Boolean = false, + val code: Int = UNKNOWN_RESPONSE_CODE, + val throwable: Throwable? = null +) { + + internal class Success(responseCode: Int) : UploadStatus(shouldRetry = false, code = responseCode) + + internal class NetworkError(throwable: Throwable) : + UploadStatus(shouldRetry = true, throwable = throwable) + + internal class DNSError(throwable: Throwable) : + UploadStatus(shouldRetry = true, throwable = throwable) + + internal class RequestCreationError(throwable: Throwable?) : + UploadStatus(shouldRetry = false, throwable = throwable) + + internal class InvalidTokenError(responseCode: Int) : UploadStatus(shouldRetry = false, code = responseCode) + internal class HttpRedirection(responseCode: Int) : UploadStatus(shouldRetry = false, code = responseCode) + internal class HttpClientError(responseCode: Int) : UploadStatus(shouldRetry = false, code = responseCode) + internal class HttpServerError(responseCode: Int) : UploadStatus(shouldRetry = true, code = responseCode) + internal class HttpClientRateLimiting(responseCode: Int) : UploadStatus(shouldRetry = true, code = responseCode) + internal class UnknownHttpError(responseCode: Int) : UploadStatus(shouldRetry = false, code = responseCode) + internal class UnknownException(throwable: Throwable) : UploadStatus(shouldRetry = true, throwable = throwable) + internal object UnknownStatus : UploadStatus(shouldRetry = false, code = UNKNOWN_RESPONSE_CODE) + + fun logStatus( + context: String, + byteSize: Int, + logger: InternalLogger, + attempts: Int, + requestId: String? = null + ) { + val level = resolveInternalLogLevel() + val targets = resolveInternalLogTarget() + logger.log( + level, + targets, + { + buildStatusMessage(requestId, byteSize, context, throwable, attempts) + } + ) + } + + private fun resolveInternalLogTarget() = when (this) { + is HttpClientError, + is HttpClientRateLimiting, + is UnknownStatus -> listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY) + + is DNSError, + is HttpRedirection, + is HttpServerError, + is InvalidTokenError, + is NetworkError, + is RequestCreationError, + is Success, + is UnknownException, + is UnknownHttpError -> listOf(InternalLogger.Target.USER) + } + + private fun resolveInternalLogLevel() = when (this) { + is HttpClientError, + is HttpServerError, + is InvalidTokenError, + is RequestCreationError, + is UnknownException, + is UnknownHttpError -> InternalLogger.Level.ERROR + + is DNSError, + is HttpClientRateLimiting, + is HttpRedirection, + is UnknownStatus, + is NetworkError -> InternalLogger.Level.WARN + + is Success -> InternalLogger.Level.INFO + } + + private fun buildStatusMessage( + requestId: String?, + byteSize: Int, + context: String, + throwable: Throwable?, + requestAttempts: Int + ): String { + val buildString = buildString { + if (requestId == null) { + append("Batch [$byteSize bytes] ($context)") + } else { + append("Batch $requestId [$byteSize bytes] ($context)") + } + + when (this@UploadStatus) { + is DNSError -> append(" failed because of a DNS error") + is HttpClientError -> append(" failed because of a processing error or invalid data") + is HttpClientRateLimiting -> append(" failed because of an intake rate limitation") + is HttpRedirection -> append(" failed because of a network redirection") + is HttpServerError -> append(" failed because of a server processing error") + is InvalidTokenError -> append(" failed because your token is invalid") + is NetworkError -> append(" failed because of a network error") + is RequestCreationError -> append(" failed because of an error when creating the request") + is UnknownException -> append(" failed because of an unknown error") + is UnknownHttpError -> append(" failed because of an unexpected HTTP error (status code = $code)") + is UnknownStatus -> append(" status is unknown") + is Success -> append(" sent successfully.") + } + + if (throwable != null) { + append(" (") + append(throwable.javaClass.name) + append(": ") + append(throwable.message) + append(")") + } + + if (shouldRetry) { + append("; we will retry later.") + } else if (this@UploadStatus !is Success) { + append("; the batch was dropped.") + } + + if (this@UploadStatus is InvalidTokenError) { + append( + " Make sure that the provided token still exists " + + "and you're targeting the relevant Datadog site." + ) + } + append( + ATTEMPTS_LOG_MESSAGE_FORMAT.format( + Locale.US, + requestAttempts, + code + ) + ) + } + return buildString + } + + companion object { + internal const val UNKNOWN_RESPONSE_CODE = 0 + internal const val ATTEMPTS_LOG_MESSAGE_FORMAT = " This request was attempted %d time(s)." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback.kt new file mode 100644 index 0000000000..5802142abf --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.lifecycle + +import android.content.Context +import androidx.work.WorkManager +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.utils.cancelUploadWorker +import com.datadog.android.core.internal.utils.triggerUploadWorker +import java.lang.ref.Reference +import java.lang.ref.WeakReference + +internal class ProcessLifecycleCallback( + appContext: Context, + internal val instanceName: String, + private val internalLogger: InternalLogger +) : + ProcessLifecycleMonitor.Callback { + + internal val contextWeakRef: Reference = WeakReference(appContext) + + override fun onStarted() { + contextWeakRef.get()?.let { + if (WorkManager.isInitialized()) { + cancelUploadWorker(it, instanceName, internalLogger) + } + } + } + + override fun onResumed() { + // NO - OP + } + + override fun onStopped() { + contextWeakRef.get()?.let { + if (WorkManager.isInitialized()) { + triggerUploadWorker(it, instanceName, internalLogger) + } + } + } + + override fun onPaused() { + // NO - OP + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitor.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitor.kt similarity index 93% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitor.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitor.kt index 48fe1a84dd..4656ba5429 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitor.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitor.kt @@ -9,6 +9,7 @@ package com.datadog.android.core.internal.lifecycle import android.app.Activity import android.app.Application import android.os.Bundle +import androidx.annotation.MainThread import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger @@ -20,6 +21,7 @@ internal class ProcessLifecycleMonitor(val callback: Callback) : val wasPaused = AtomicBoolean(true) val wasStopped = AtomicBoolean(true) + @MainThread override fun onActivityPaused(activity: Activity) { if (activitiesResumedCounter.decrementAndGet() == 0 && !wasPaused.getAndSet(true) @@ -29,6 +31,7 @@ internal class ProcessLifecycleMonitor(val callback: Callback) : } } + @MainThread override fun onActivityStarted(activity: Activity) { if (activitiesStartedCounter.incrementAndGet() == 1 && wasStopped.getAndSet(false) @@ -38,14 +41,17 @@ internal class ProcessLifecycleMonitor(val callback: Callback) : } } + @MainThread override fun onActivityDestroyed(activity: Activity) { // NO-OP } + @MainThread override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { // NO-OP } + @MainThread override fun onActivityStopped(activity: Activity) { if (activitiesStartedCounter.decrementAndGet() == 0 && wasPaused.get()) { // trigger on process stopped @@ -54,10 +60,12 @@ internal class ProcessLifecycleMonitor(val callback: Callback) : } } + @MainThread override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { // NO-OP } + @MainThread override fun onActivityResumed(activity: Activity) { if (activitiesResumedCounter.incrementAndGet() == 1 && wasPaused.getAndSet(false) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/logger/LogcatLogHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/logger/LogcatLogHandler.kt new file mode 100644 index 0000000000..f349ab3453 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/logger/LogcatLogHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.logger + +import android.os.Build +import android.util.Log + +internal class LogcatLogHandler( + internal val tag: String, + internal val predicate: (level: Int) -> Boolean = { true } +) { + + fun canLog(level: Int): Boolean { + return predicate(level) + } + + fun log(level: Int, message: String, throwable: Throwable?) { + if (!predicate.invoke(level)) return + + val tag = resolveTag() + Log.println(level, tag, message) + if (throwable != null) { + Log.println( + level, + tag, + Log.getStackTraceString(throwable) + ) + } + } + + private fun resolveTag(): String { + return if (tag.length >= MAX_TAG_LENGTH && + Build.VERSION.SDK_INT < Build.VERSION_CODES.N + ) { + @Suppress("UnsafeThirdPartyFunctionCall") + // substring can't throw IndexOutOfBounds, we checked the length + tag.substring(0, MAX_TAG_LENGTH) + } else { + tag + } + } + + companion object { + private const val MAX_TAG_LENGTH = 23 + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/logger/SdkInternalLogger.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/logger/SdkInternalLogger.kt new file mode 100644 index 0000000000..9505a5b00e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/logger/SdkInternalLogger.kt @@ -0,0 +1,283 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.logger + +import android.util.Log +import com.datadog.android.BuildConfig +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry +import com.datadog.android.core.metrics.PerformanceMetric +import com.datadog.android.core.metrics.TelemetryMetricType +import com.datadog.android.core.sampling.RateBasedSampler +import com.datadog.android.internal.attributes.LocalAttribute +import com.datadog.android.internal.attributes.enrichWithNonNullAttribute +import com.datadog.android.internal.telemetry.InternalTelemetryEvent + +internal class SdkInternalLogger( + private val sdkCore: FeatureSdkCore?, + userLogHandlerFactory: () -> LogcatLogHandler = { + LogcatLogHandler(DEV_LOG_TAG) { level -> + level >= Datadog.getVerbosity() + } + }, + maintainerLogHandlerFactory: () -> LogcatLogHandler? = { + if (BuildConfig.LOGCAT_ENABLED) { + LogcatLogHandler(SDK_LOG_TAG) + } else { + null + } + } +) : InternalLogger { + + /** + * This logger is meant for user's debugging purposes. + * Logcat logs are conditioned by the [Datadog.libraryVerbosity]. + * No Datadog logs should be sent. + */ + internal val userLogger = userLogHandlerFactory.invoke() + + /** + * This logger is meant for internal debugging purposes. + * Logcat logs are conditioned by a BuildConfig flag (set to false for releases). + */ + internal val maintainerLogger = maintainerLogHandlerFactory.invoke() + + private val onlyOnceUserMessages = mutableSetOf() + private val onlyOnceMaintainerMessages = mutableSetOf() + private val onlyOnceTelemetryMessages = mutableSetOf() + + // region InternalLogger + + override fun log( + level: InternalLogger.Level, + target: InternalLogger.Target, + messageBuilder: () -> String, + throwable: Throwable?, + onlyOnce: Boolean, + additionalProperties: Map? + ) { + when (target) { + InternalLogger.Target.USER -> logToUser(level, messageBuilder, throwable, onlyOnce) + InternalLogger.Target.MAINTAINER -> logToMaintainer( + level, + messageBuilder, + throwable, + onlyOnce + ) + + InternalLogger.Target.TELEMETRY -> logToTelemetry( + level, + messageBuilder, + throwable, + onlyOnce, + additionalProperties + ) + } + } + + override fun log( + level: InternalLogger.Level, + targets: List, + messageBuilder: () -> String, + throwable: Throwable?, + onlyOnce: Boolean, + additionalProperties: Map? + ) { + targets.forEach { + log(level, it, messageBuilder, throwable, onlyOnce, additionalProperties) + } + } + + override fun logMetric( + messageBuilder: () -> String, + additionalProperties: Map, + samplingRate: Float, + creationSampleRate: Float? + ) { + if (!sample(samplingRate)) return + val rumFeature = sdkCore?.getFeature(Feature.RUM_FEATURE_NAME) ?: return + val additionalPropertiesMutable = additionalProperties.toMutableMap() + .enrichWithNonNullAttribute( + LocalAttribute.Key.CREATION_SAMPLING_RATE, + creationSampleRate + ) + .enrichWithNonNullAttribute( + LocalAttribute.Key.REPORTING_SAMPLING_RATE, + samplingRate + ) + + val metricEvent = InternalTelemetryEvent.Metric( + message = messageBuilder(), + additionalProperties = additionalPropertiesMutable + ) + rumFeature.sendEvent(metricEvent) + } + + override fun startPerformanceMeasure( + callerClass: String, + metric: TelemetryMetricType, + samplingRate: Float, + operationName: String + ): PerformanceMetric? { + if (!sample(samplingRate)) return null + + return when (metric) { + TelemetryMetricType.MethodCalled -> { + MethodCalledTelemetry( + internalLogger = this, + operationName = operationName, + callerClass = callerClass, + creationSampleRate = samplingRate + ) + } + } + } + + override fun logApiUsage( + samplingRate: Float, + apiUsageEventBuilder: () -> InternalTelemetryEvent.ApiUsage + ) { + if (!sample(samplingRate)) return + val rumFeature = sdkCore?.getFeature(Feature.RUM_FEATURE_NAME) ?: return + + val event = apiUsageEventBuilder() + event.additionalProperties.enrichWithNonNullAttribute( + LocalAttribute.Key.REPORTING_SAMPLING_RATE, + samplingRate + ) + + rumFeature.sendEvent(event) + } + + // endregion + + // region Internal + + fun sample(samplingRate: Float): Boolean { + return RateBasedSampler(samplingRate).sample(Unit) + } + + private fun logToUser( + level: InternalLogger.Level, + messageBuilder: () -> String, + error: Throwable?, + onlyOnce: Boolean + ) { + sendToLogHandler( + userLogger, + level, + messageBuilder, + error, + onlyOnce, + onlyOnceUserMessages + ) + } + + private fun logToMaintainer( + level: InternalLogger.Level, + messageBuilder: () -> String, + error: Throwable?, + onlyOnce: Boolean + ) { + maintainerLogger?.let { + sendToLogHandler( + it, + level, + messageBuilder, + error, + onlyOnce, + onlyOnceMaintainerMessages + ) + } + } + + private fun sendToLogHandler( + handler: LogcatLogHandler, + level: InternalLogger.Level, + messageBuilder: () -> String, + error: Throwable?, + onlyOnce: Boolean, + knownSingleMessages: MutableSet + ) { + if (!handler.canLog(level.toLogLevel())) return + val message = messageBuilder().withSdkName() + if (onlyOnce) { + if (knownSingleMessages.contains(message)) { + // drop the message… wait should we log that we dropped it? + return + } else { + knownSingleMessages.add(message) + } + } + handler.log(level.toLogLevel(), message, error) + } + + private fun logToTelemetry( + level: InternalLogger.Level, + messageBuilder: () -> String, + error: Throwable?, + onlyOnce: Boolean, + additionalProperties: Map? + ) { + val rumFeature = sdkCore?.getFeature(Feature.RUM_FEATURE_NAME) ?: return + val message = messageBuilder() + if (onlyOnce) { + if (onlyOnceTelemetryMessages.contains(message)) { + // drop the message… wait should we log that we dropped it? + return + } else { + onlyOnceTelemetryMessages.add(message) + } + } + val telemetryEvent = if ( + level == InternalLogger.Level.ERROR || + level == InternalLogger.Level.WARN || + error != null + ) { + InternalTelemetryEvent.Log.Error( + message = message, + additionalProperties = additionalProperties, + error = error + ) + } else { + InternalTelemetryEvent.Log.Debug( + message = message, + additionalProperties = additionalProperties + ) + } + rumFeature.sendEvent(telemetryEvent) + } + + private fun InternalLogger.Level.toLogLevel(): Int { + return when (this) { + InternalLogger.Level.VERBOSE -> Log.VERBOSE + InternalLogger.Level.DEBUG -> Log.DEBUG + InternalLogger.Level.INFO -> Log.INFO + InternalLogger.Level.WARN -> Log.WARN + InternalLogger.Level.ERROR -> Log.ERROR + } + } + + private fun String.withSdkName(): String { + val instanceName = sdkCore?.name + return if (instanceName != null) { + "[$instanceName]: $this" + } else { + this + } + } + + companion object { + internal const val SDK_LOG_TAG = "DD_LOG" + internal const val DEV_LOG_TAG = "Datadog" + } + + // endregion +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchClosedMetadata.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchClosedMetadata.kt new file mode 100644 index 0000000000..73f04ee4f9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchClosedMetadata.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +internal data class BatchClosedMetadata( + internal val lastTimeWasUsedInMs: Long, + internal val eventsCount: Long +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcher.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcher.kt new file mode 100644 index 0000000000..8b739bae42 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcher.kt @@ -0,0 +1,264 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.advanced.FeatureFileOrchestrator +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.lengthSafe +import com.datadog.android.internal.time.TimeProvider +import com.datadog.android.privacy.TrackingConsent +import java.io.File +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean + +internal class BatchMetricsDispatcher( + featureName: String, + private val uploadConfiguration: DataUploadConfiguration?, + private val filePersistenceConfig: FilePersistenceConfig, + private val internalLogger: InternalLogger, + private val dateTimeProvider: TimeProvider + +) : MetricsDispatcher, ProcessLifecycleMonitor.Callback { + + private val trackName: String? = resolveTrackName(featureName) + private val isInBackground = AtomicBoolean(true) + + // region MetricsDispatcher + + override fun sendBatchDeletedMetric(batchFile: File, removalReason: RemovalReason, numPendingBatches: Int) { + if (!removalReason.includeInMetrics() || trackName == null) { + return + } + resolveBatchDeletedMetricAttributes(batchFile, removalReason, numPendingBatches)?.let { + internalLogger.logMetric( + messageBuilder = { BATCH_DELETED_MESSAGE }, + additionalProperties = it, + samplingRate = 1.5f + ) + } + } + + override fun sendBatchClosedMetric(batchFile: File, batchMetadata: BatchClosedMetadata) { + if (trackName == null || !batchFile.existsSafe(internalLogger)) { + return + } + resolveBatchClosedMetricAttributes(batchFile, batchMetadata)?.let { + internalLogger.logMetric( + messageBuilder = { BATCH_CLOSED_MESSAGE }, + additionalProperties = it, + samplingRate = 1.5f + ) + } + } + + // endregion + + // region ProcessLifecycleMonitor.Callback + override fun onStarted() { + // NO - OP + } + + override fun onResumed() { + isInBackground.set(false) + } + + override fun onStopped() { + // NO - OP + } + + override fun onPaused() { + isInBackground.set(true) + } + + // endregion + + // region Internal + + @SuppressWarnings("ReturnCount") + private fun resolveBatchDeletedMetricAttributes( + file: File, + deletionReason: RemovalReason, + numPendingBatches: Int + ): Map? { + val fileCreationTimestamp = file.nameAsTimestampSafe(internalLogger) ?: return null + val fileAgeInMillis = dateTimeProvider.getDeviceTimestamp() - fileCreationTimestamp + if (fileAgeInMillis < 0) { + // the device time was manually modified or the time zone changed + // we are dropping this metric to not skew our charts + return null + } + return mapOf( + TRACK_KEY to trackName, + TYPE_KEY to BATCH_DELETED_TYPE_VALUE, + BATCH_AGE_KEY to fileAgeInMillis, + UPLOADER_DELAY_KEY to mapOf( + UPLOADER_DELAY_MIN_KEY to uploadConfiguration?.minDelayMs, + UPLOADER_DELAY_MAX_KEY to uploadConfiguration?.maxDelayMs + ), + UPLOADER_WINDOW_KEY to filePersistenceConfig.recentDelayMs, + + BATCH_REMOVAL_KEY to deletionReason.toString(), + IN_BACKGROUND_KEY to isInBackground.get(), + TRACKING_CONSENT_KEY to file.resolveFileOriginAsConsent(), + FILE_NAME to file.name, + PENDING_BATCHES to numPendingBatches, + THREAD_NAME to Thread.currentThread().name + ) + } + + @SuppressWarnings("ReturnCount") + private fun resolveBatchClosedMetricAttributes( + file: File, + batchMetadata: BatchClosedMetadata + ): Map? { + val fileCreationTimestamp = file.nameAsTimestampSafe(internalLogger) ?: return null + val batchDurationInMs = batchMetadata.lastTimeWasUsedInMs - fileCreationTimestamp + if (batchDurationInMs < 0) { + // the device time was manually modified or the time zone changed + // we are dropping this metric to not skew our charts + return null + } + return mapOf( + TRACK_KEY to trackName, + TYPE_KEY to BATCH_CLOSED_TYPE_VALUE, + BATCH_DURATION_KEY to batchDurationInMs, + UPLOADER_WINDOW_KEY to filePersistenceConfig.recentDelayMs, + // we will send the telemetry even if the file is broken as it will still + // be sent as a batch_delete telemetry later + BATCH_SIZE_KEY to file.lengthSafe(internalLogger), + BATCH_EVENTS_COUNT_KEY to batchMetadata.eventsCount, + TRACKING_CONSENT_KEY to file.resolveFileOriginAsConsent(), + FILE_NAME to file.name, + THREAD_NAME to Thread.currentThread().name + ) + } + + private fun File.nameAsTimestampSafe(logger: InternalLogger): Long? { + val timestamp = this.name.toLongOrNull() + if (timestamp == null) { + logger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { WRONG_FILE_NAME_MESSAGE_FORMAT.format(Locale.ENGLISH, this.name) } + ) + } + return timestamp + } + + private fun resolveTrackName(featureName: String): String? { + return when (featureName) { + Feature.RUM_FEATURE_NAME -> RUM_TRACK_NAME + Feature.LOGS_FEATURE_NAME -> LOGS_TRACK_NAME + Feature.TRACING_FEATURE_NAME -> TRACE_TRACK_NAME + Feature.SESSION_REPLAY_FEATURE_NAME -> SR_TRACK_NAME + Feature.SESSION_REPLAY_RESOURCES_FEATURE_NAME -> SR_RESOURCES_TRACK_NAME + + else -> null + } + } + + private fun File.resolveFileOriginAsConsent(): String? { + val fileDirectory = this.parentFile?.name ?: return null + return if (fileDirectory.matches(FeatureFileOrchestrator.IS_PENDING_DIR_REG_EX)) { + TrackingConsent.PENDING.toString().lowercase(Locale.US) + } else if (fileDirectory.matches(FeatureFileOrchestrator.IS_GRANTED_DIR_REG_EX)) { + TrackingConsent.GRANTED.toString().lowercase(Locale.US) + } else { + null + } + } + + // endregion + + companion object { + + internal const val RUM_TRACK_NAME = "rum" + internal const val LOGS_TRACK_NAME = "logs" + internal const val TRACE_TRACK_NAME = "trace" + internal const val SR_TRACK_NAME = "sr" + internal const val SR_RESOURCES_TRACK_NAME = "sr-resources" + + internal const val WRONG_FILE_NAME_MESSAGE_FORMAT = + "Unable to parse the file name as a timestamp: %s" + + // region COMMON METRIC KEYS + + /* The key for the type of the metric.*/ + internal const val TYPE_KEY = "metric_type" + + /* The key for the track name.*/ + internal const val TRACK_KEY = "track" + + /* The default duration since last write (in ms) after which the uploader considers + the file to be "ready for upload".*/ + internal const val UPLOADER_WINDOW_KEY = "uploader_window" + + // endregion + + // region BATCH DELETE METRIC KEYS AND VALUES + + /* The key for uploader's delay options.*/ + internal const val UPLOADER_DELAY_KEY = "uploader_delay" + + /* The min delay of uploads for this track (in ms).*/ + internal const val UPLOADER_DELAY_MAX_KEY = "max" + + /* The min delay of uploads for this track (in ms).*/ + internal const val UPLOADER_DELAY_MIN_KEY = "min" + + /* The duration from batch creation to batch deletion (in ms).*/ + internal const val BATCH_AGE_KEY = "batch_age" + + /* The reason of batch deletion. */ + internal const val BATCH_REMOVAL_KEY = "batch_removal_reason" + + /* The number of still unsent batch files */ + internal const val PENDING_BATCHES = "pending_batches" + + /* If the batch was deleted in the background. */ + internal const val IN_BACKGROUND_KEY = "in_background" + + internal const val BATCH_DELETED_MESSAGE = "[Mobile Metric] Batch Deleted" + + /* The value for the type of the metric.*/ + internal const val BATCH_DELETED_TYPE_VALUE = "batch deleted" + + // endregion + + // region BATCH CLOSE METRIC KEYS AND VALUES + + /* The size of batch at closing (in bytes). */ + internal const val BATCH_SIZE_KEY = "batch_size" + + /* The number of events written to this batch before closing.*/ + internal const val BATCH_EVENTS_COUNT_KEY = "batch_events_count" + + /* The duration from batch creation to batch closing (in ms).*/ + internal const val BATCH_DURATION_KEY = "batch_duration" + + internal const val BATCH_CLOSED_MESSAGE = "[Mobile Metric] Batch Closed" + + /* The value for the type of the metric.*/ + internal const val BATCH_CLOSED_TYPE_VALUE = "batch closed" + + /* The value of the tracking consent according with this file origin.*/ + internal const val TRACKING_CONSENT_KEY = "consent" + + /* The file name.*/ + internal const val FILE_NAME = "filename" + + /* The thread name from which the current metric was sent.*/ + internal const val THREAD_NAME = "thread" + + // endregion + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BenchmarkUploads.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BenchmarkUploads.kt new file mode 100644 index 0000000000..38a6611b14 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BenchmarkUploads.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +import com.datadog.android.internal.profiler.BenchmarkSdkUploads +import com.datadog.android.internal.profiler.GlobalBenchmark + +internal class BenchmarkUploads( + private val benchmarkSdkUploads: BenchmarkSdkUploads = GlobalBenchmark.getBenchmarkSdkUploads() +) { + + internal fun sendBenchmarkBytesUploaded( + featureName: String, + value: Long + ) { + sendBenchmarkUploads( + featureName = featureName, + metricName = BENCHMARK_BYTES_UPLOADED, + value = value + ) + } + + internal fun sendBenchmarkBytesDeleted( + featureName: String, + value: Long + ) { + sendBenchmarkUploads( + featureName = featureName, + metricName = BENCHMARK_BYTES_DELETED, + value = value + ) + } + + internal fun sendBenchmarkBytesWritten( + featureName: String, + value: Long + ) { + sendBenchmarkUploads( + featureName = featureName, + metricName = BENCHMARK_BYTES_WRITTEN, + value = value + ) + } + + internal fun incrementBenchmarkUploadsCount( + featureName: String + ) { + sendBenchmarkUploads( + featureName = featureName, + metricName = BENCHMARK_UPLOAD_COUNT, + value = 1 + ) + } + + private fun sendBenchmarkUploads( + featureName: String, + metricName: String, + value: Long + ) { + val tags = mapOf( + TRACK_NAME to featureName + ) + + benchmarkSdkUploads + .getMeter(METER_NAME) + .getCounter(metricName) + .add(value, tags) + } + + internal companion object { + private const val TRACK_NAME = "track" + private const val METER_NAME = "dd-sdk-android" + internal const val BENCHMARK_BYTES_UPLOADED = "android.benchmark.bytes_uploaded" + internal const val BENCHMARK_UPLOAD_COUNT = "android.benchmark.upload_count" + internal const val BENCHMARK_BYTES_WRITTEN = "android.benchmark.bytes_written" + internal const val BENCHMARK_BYTES_DELETED = "android.benchmark.bytes_deleted" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/MethodCalledTelemetry.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/MethodCalledTelemetry.kt new file mode 100644 index 0000000000..1bb0c6d646 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/MethodCalledTelemetry.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.metrics.MethodCallSamplingRate +import com.datadog.android.core.metrics.PerformanceMetric +import com.datadog.android.core.metrics.PerformanceMetric.Companion.METRIC_TYPE + +/** + * Performance metric to measure the execution time for a method. + * @param internalLogger - an instance of the internal logger. + * @param operationName the operation name + * @param callerClass - the class calling the performance metric. + * @param creationSampleRate - sampling frequency used to create the metric + * @param startTime - the time when the metric is instantiated, to be used as the start point for the measurement. + */ +internal class MethodCalledTelemetry( + internal val internalLogger: InternalLogger, + internal val operationName: String, + internal val callerClass: String, + internal val creationSampleRate: Float, + internal val startTime: Long = System.nanoTime() +) : PerformanceMetric { + + override fun stopAndSend(isSuccessful: Boolean) { + val executionTime = System.nanoTime() - startTime + val additionalProperties: MutableMap = mutableMapOf() + + additionalProperties[EXECUTION_TIME] = executionTime + additionalProperties[OPERATION_NAME] = operationName + additionalProperties[CALLER_CLASS] = callerClass + additionalProperties[IS_SUCCESSFUL] = isSuccessful + additionalProperties[METRIC_TYPE] = METRIC_TYPE_VALUE + + internalLogger.logMetric( + messageBuilder = { METHOD_CALLED_METRIC_NAME }, + additionalProperties = additionalProperties, + samplingRate = MethodCallSamplingRate.ALL.rate, // sampling is performed on start + creationSampleRate = creationSampleRate + ) + } + + companion object { + /** + * Title of the metric to be sent. + */ + const val METHOD_CALLED_METRIC_NAME: String = "[Mobile Metric] Method Called" + + /** + * Metric type value. + */ + const val METRIC_TYPE_VALUE: String = "method called" + + /** + * The key for operation name. + */ + const val OPERATION_NAME: String = "operation_name" + + /** + * The key for caller class. + */ + const val CALLER_CLASS: String = "caller_class" + + /** + * The key for is successful. + */ + const val IS_SUCCESSFUL: String = "is_successful" + + /** + * The key for execution time. + */ + const val EXECUTION_TIME: String = "execution_time" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/MetricsDispatcher.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/MetricsDispatcher.kt new file mode 100644 index 0000000000..9a21cbdacc --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/MetricsDispatcher.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +import com.datadog.tools.annotation.NoOpImplementation +import java.io.File + +@NoOpImplementation +internal interface MetricsDispatcher { + fun sendBatchDeletedMetric(batchFile: File, removalReason: RemovalReason, numPendingBatches: Int) + + fun sendBatchClosedMetric(batchFile: File, batchMetadata: BatchClosedMetadata) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/RemovalReason.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/RemovalReason.kt new file mode 100644 index 0000000000..8fae3aa693 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/RemovalReason.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +internal sealed class RemovalReason { + + internal fun includeInMetrics(): Boolean { + return this !is Flushed + } + internal data class IntakeCode(private val responseCode: Int) : RemovalReason() { + override fun toString(): String { + return "intake-code-$responseCode" + } + } + + internal object Invalid : RemovalReason() { + override fun toString(): String { + return "invalid" + } + } + + internal object Purged : RemovalReason() { + override fun toString(): String { + return "purged" + } + } + + internal object Obsolete : RemovalReason() { + override fun toString(): String { + return "obsolete" + } + } + + internal object Flushed : RemovalReason() { + override fun toString(): String { + return "flushed" + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver.kt new file mode 100644 index 0000000000..dfbcd02778 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver.kt @@ -0,0 +1,84 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.net + +import com.datadog.android.lint.InternalApi +import com.datadog.android.trace.TracingHeaderType +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.util.Locale + +/** + * Default implementation of [FirstPartyHostHeaderTypeResolver]. + * + * @param hosts [Map] of hosts and associated tracing header types to initialize instance with. + */ +@InternalApi +class DefaultFirstPartyHostHeaderTypeResolver( + hosts: Map> +) : FirstPartyHostHeaderTypeResolver { + + internal var knownHosts = hosts.entries.associate { it.key.lowercase(Locale.US) to it.value } + private set + + /** @inheritdoc */ + override fun isFirstPartyUrl(url: HttpUrl): Boolean { + val host = url.host + return knownHosts.keys.any { + it == "*" || host == it || host.endsWith(".$it") + } + } + + /** @inheritdoc */ + override fun isFirstPartyUrl(url: String): Boolean { + val httpUrl = url.toHttpUrlOrNull() ?: return false + return isFirstPartyUrl(httpUrl) + } + + /** @inheritdoc */ + override fun headerTypesForUrl(url: String): Set { + val httpUrl = url.toHttpUrlOrNull() ?: return emptySet() + return headerTypesForUrl(httpUrl) + } + + /** @inheritdoc */ + override fun headerTypesForUrl(url: HttpUrl): Set { + val host = url.host + + return knownHosts[host] + ?: knownHosts.entries.firstOrNull { host.endsWith(".${it.key}") }?.value + ?: knownHosts["*"] + ?: emptySet() + } + + /** @inheritdoc */ + override fun getAllHeaderTypes(): Set { + return knownHosts.values.flatten().toSet() + } + + /** @inheritdoc */ + override fun isEmpty(): Boolean { + return knownHosts.isEmpty() + } + + internal fun addKnownHosts(hosts: List) { + knownHosts = knownHosts + hosts.associate { + it.lowercase(Locale.US) to setOf( + TracingHeaderType.DATADOG, + TracingHeaderType.TRACECONTEXT + ) + } + } + + internal fun addKnownHostsWithHeaderTypes( + hostsWithHeaderTypes: Map> + ) { + knownHosts = knownHosts + hostsWithHeaderTypes.entries.associate { + it.key.lowercase(Locale.US) to it.value + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver.kt new file mode 100644 index 0000000000..6575d59b81 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.net + +import com.datadog.android.trace.TracingHeaderType +import okhttp3.HttpUrl + +/** + * Interface to be implemented by the class which wants to check if the given url is first party + * and if there is any tracing header types associated with it. + */ +@Suppress("PackageNameVisibility") // Can't mark it as @InternalApi as it would apply to implementations as well +interface FirstPartyHostHeaderTypeResolver { + + /** + * Check if given URL is first party. + * + * @param url URL to check. + */ + fun isFirstPartyUrl(url: HttpUrl): Boolean + + /** + * Check if given URL is first party. + * + * @param url URL to check. + */ + fun isFirstPartyUrl(url: String): Boolean + + /** + * Returns the set of tracing header types associated with given URL. + * + * @param url URL to check. + */ + fun headerTypesForUrl(url: String): Set + + /** + * Returns the set of tracing header types associated with given URL. + * + * @param url URL to check. + */ + fun headerTypesForUrl(url: HttpUrl): Set + + /** + * Returns all tracing header types registered. + */ + fun getAllHeaderTypes(): Set + + /** + * Shows if resolver has any first party URLs registered or not. + */ + fun isEmpty(): Boolean +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt similarity index 89% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt index 1cf8be20f9..60a5ea9acc 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +@file:Suppress("DEPRECATION") + package com.datadog.android.core.internal.net.info import android.annotation.SuppressLint @@ -11,25 +13,26 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager -import android.net.NetworkInfo as AndroidNetworkInfo import android.os.Build import android.telephony.TelephonyManager +import com.datadog.android.api.context.NetworkInfo import com.datadog.android.core.internal.receiver.ThreadSafeReceiver -import com.datadog.android.core.internal.utils.sdkLogger +import com.datadog.android.core.internal.system.BuildSdkVersionProvider +import android.net.NetworkInfo as AndroidNetworkInfo @Suppress("DEPRECATION") @SuppressLint("InlinedApi") -internal class BroadcastReceiverNetworkInfoProvider : +internal class BroadcastReceiverNetworkInfoProvider( + private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT +) : ThreadSafeReceiver(), NetworkInfoProvider { - private var networkInfo: NetworkInfo = - NetworkInfo() + private var networkInfo: NetworkInfo = NetworkInfo() // region BroadcastReceiver override fun onReceive(context: Context, intent: Intent?) { - sdkLogger.d("received network update") val connectivityMgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager val activeNetworkInfo = connectivityMgr?.activeNetworkInfo @@ -43,7 +46,9 @@ internal class BroadcastReceiverNetworkInfoProvider : override fun register(context: Context) { val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) - registerReceiver(context, filter).also { onReceive(context, it) } + registerReceiver(context, filter).also { + onReceive(context, it) + } } override fun unregister(context: Context) { @@ -93,11 +98,11 @@ internal class BroadcastReceiverNetworkInfoProvider : } val cellularTechnology = getCellularTechnology(subtype) - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return if (buildSdkVersionProvider.version >= Build.VERSION_CODES.P) { val telephonyMgr = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager val carrierName = telephonyMgr?.simCarrierIdName ?: UNKNOWN_CARRIER_NAME - val carrierId = telephonyMgr?.simCarrierId ?: -1 + val carrierId = telephonyMgr?.simCarrierId?.toLong() NetworkInfo( connectivity, carrierName.toString(), @@ -129,7 +134,7 @@ internal class BroadcastReceiverNetworkInfoProvider : TelephonyManager.NETWORK_TYPE_TD_SCDMA -> "TD_SCDMA" TelephonyManager.NETWORK_TYPE_LTE -> "LTE" TelephonyManager.NETWORK_TYPE_IWLAN -> "IWLAN" - 19 -> "LTE_CA" + NETWORK_TYPE_LTE_CA -> "LTE_CA" TelephonyManager.NETWORK_TYPE_NR -> "New Radio" else -> null } @@ -139,6 +144,8 @@ internal class BroadcastReceiverNetworkInfoProvider : companion object { + const val NETWORK_TYPE_LTE_CA = 19 // @Hide TelephonyManager.NETWORK_TYPE_LTE_CA, + private val knownMobileTypes = setOf( ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_MOBILE_DUN, @@ -172,7 +179,7 @@ internal class BroadcastReceiverNetworkInfoProvider : private val known4GSubtypes = setOf( TelephonyManager.NETWORK_TYPE_LTE, TelephonyManager.NETWORK_TYPE_IWLAN, - 19 // @Hide TelephonyManager.NETWORK_TYPE_LTE_CA, + NETWORK_TYPE_LTE_CA ) private val known5GSubtypes = setOf( diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt new file mode 100644 index 0000000000..d7d2f248b9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt @@ -0,0 +1,189 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.net.info + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Build +import androidx.annotation.RequiresApi +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.core.internal.system.BuildSdkVersionProvider + +@RequiresApi(Build.VERSION_CODES.N) +internal class CallbackNetworkInfoProvider( + private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT, + private val internalLogger: InternalLogger +) : + ConnectivityManager.NetworkCallback(), + NetworkInfoProvider { + + private var lastNetworkInfo: NetworkInfo = NetworkInfo() + + // region NetworkCallback + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + super.onCapabilitiesChanged(network, networkCapabilities) + lastNetworkInfo = NetworkInfo( + connectivity = getNetworkType(networkCapabilities), + upKbps = resolveUpBandwidth(networkCapabilities), + downKbps = resolveDownBandwidth(networkCapabilities), + strength = resolveStrength(networkCapabilities) + ) + } + + override fun onLost(network: Network) { + super.onLost(network) + lastNetworkInfo = NetworkInfo(connectivity = NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + } + + // endregion + + //region NetworkInfoProvider + + @Suppress("TooGenericExceptionCaught") + override fun register(context: Context) { + val systemService = context.getSystemService(Context.CONNECTIVITY_SERVICE) + val connMgr = systemService as? ConnectivityManager + + if (connMgr == null) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_REGISTER } + ) + return + } + + try { + connMgr.registerDefaultNetworkCallback(this) + val activeNetwork = connMgr.activeNetwork + val activeCaps = connMgr.getNetworkCapabilities(activeNetwork) + if (activeNetwork != null && activeCaps != null) { + onCapabilitiesChanged(activeNetwork, activeCaps) + } + } catch (e: SecurityException) { + // RUMM-852 On some devices we get a SecurityException with message + // "package does not belong to xxxx" + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_REGISTER }, + e + ) + lastNetworkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_OTHER) + } catch (e: Exception) { + // RUMM-918 in some cases the device throws a IllegalArgumentException on register + // "Too many NetworkRequests filed" This happens when registerDefaultNetworkCallback is + // called too many times without matching unregisterNetworkCallback + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_REGISTER }, + e + ) + lastNetworkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_OTHER) + } + } + + @Suppress("TooGenericExceptionCaught") + override fun unregister(context: Context) { + val systemService = context.getSystemService(Context.CONNECTIVITY_SERVICE) + val connMgr = systemService as? ConnectivityManager + + if (connMgr == null) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_UNREGISTER } + ) + return + } + + try { + connMgr.unregisterNetworkCallback(this) + } catch (e: SecurityException) { + // RUMM-852 On some devices we get a SecurityException with message + // "package does not belong to xxxx" + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_UNREGISTER }, + e + ) + } catch (e: RuntimeException) { + // RUMM-918 in some cases the device throws a IllegalArgumentException on unregister + // e.g. when the callback was not registered + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_UNREGISTER }, + e + ) + } + } + + override fun getLatestNetworkInfo(): NetworkInfo { + return lastNetworkInfo + } + + // endregion + + // region Internal + + private fun resolveUpBandwidth(networkCapabilities: NetworkCapabilities): Long? { + return if (networkCapabilities.linkUpstreamBandwidthKbps > 0) { + networkCapabilities.linkUpstreamBandwidthKbps.toLong() + } else { + null + } + } + + private fun resolveDownBandwidth(networkCapabilities: NetworkCapabilities): Long? { + return if (networkCapabilities.linkDownstreamBandwidthKbps > 0) { + networkCapabilities.linkDownstreamBandwidthKbps.toLong() + } else { + null + } + } + + @SuppressLint("NewApi") + private fun resolveStrength(networkCapabilities: NetworkCapabilities): Long? { + return if (buildSdkVersionProvider.version >= Build.VERSION_CODES.Q && + networkCapabilities.signalStrength != NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED + ) { + networkCapabilities.signalStrength.toLong() + } else { + null + } + } + + private fun getNetworkType(networkCapabilities: NetworkCapabilities): NetworkInfo.Connectivity { + return if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + NetworkInfo.Connectivity.NETWORK_WIFI + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + NetworkInfo.Connectivity.NETWORK_ETHERNET + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + NetworkInfo.Connectivity.NETWORK_CELLULAR + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) { + NetworkInfo.Connectivity.NETWORK_BLUETOOTH + } else { + NetworkInfo.Connectivity.NETWORK_OTHER + } + } + + // endregion + + companion object { + internal const val ERROR_REGISTER = "We couldn't register a Network Callback, " + + "the network information reported will be less accurate." + internal const val ERROR_UNREGISTER = "We couldn't unregister the Network Callback" + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfoProvider.kt similarity index 91% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfoProvider.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfoProvider.kt index 313c8a2c0f..79d4ccc63d 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfoProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfoProvider.kt @@ -7,6 +7,7 @@ package com.datadog.android.core.internal.net.info import android.content.Context +import com.datadog.android.api.context.NetworkInfo import com.datadog.tools.annotation.NoOpImplementation @NoOpImplementation diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/AbstractStorage.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/AbstractStorage.kt new file mode 100644 index 0000000000..9d1b9ab642 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/AbstractStorage.kt @@ -0,0 +1,146 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.core.persistence.NoOpPersistenceStrategy +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.privacy.TrackingConsentProviderCallback +import java.util.concurrent.ExecutorService + +internal class AbstractStorage( + internal val sdkCoreId: String?, + private val featureName: String, + internal val persistenceStrategyFactory: PersistenceStrategy.Factory, + private val executorService: ExecutorService, + private val internalLogger: InternalLogger, + internal val storageConfiguration: FeatureStorageConfiguration, + consentProvider: ConsentProvider +) : Storage, TrackingConsentProviderCallback { + + private val grantedPersistenceStrategy: PersistenceStrategy by lazy { + persistenceStrategyFactory.create( + "$sdkCoreId/$featureName/${TrackingConsent.GRANTED}", + storageConfiguration.maxItemsPerBatch, + storageConfiguration.maxBatchSize + ) + } + + private val pendingPersistenceStrategy: PersistenceStrategy by lazy { + persistenceStrategyFactory.create( + "$sdkCoreId/$featureName/${TrackingConsent.PENDING}", + storageConfiguration.maxItemsPerBatch, + storageConfiguration.maxBatchSize + ) + } + + private val writeLock = Any() + + private val notGrantedPersistenceStrategy: PersistenceStrategy = NoOpPersistenceStrategy() + + init { + @Suppress("LeakingThis") + consentProvider.registerCallback(this) + } + + // region Storage + + @AnyThread + override fun getEventWriteScope( + datadogContext: DatadogContext + ): EventWriteScope { + val strategy = resolvePersistenceStrategy(datadogContext) + val writer = object : EventBatchWriter { + @WorkerThread + override fun currentMetadata(): ByteArray? { + return strategy.currentMetadata() + } + + @WorkerThread + override fun write(event: RawBatchEvent, batchMetadata: ByteArray?, eventType: EventType): Boolean { + return strategy.write(event, batchMetadata, eventType) + } + } + // although we don't know what storage is backed by the persistence strategy, so maybe writing in a concurrent + // way is fine there and lock is not needed, but taking precautions + return AsyncEventWriteScope(executorService, writer, writeLock, featureName, internalLogger) + } + + private fun resolvePersistenceStrategy(datadogContext: DatadogContext) = + when (datadogContext.trackingConsent) { + TrackingConsent.GRANTED -> grantedPersistenceStrategy + TrackingConsent.PENDING -> pendingPersistenceStrategy + TrackingConsent.NOT_GRANTED -> notGrantedPersistenceStrategy + } + + @WorkerThread + override fun readNextBatch(): BatchData? { + return grantedPersistenceStrategy.lockAndReadNext()?.let { + BatchData( + id = BatchId(it.batchId), + data = it.events, + metadata = it.metadata + ) + } + } + + @WorkerThread + override fun confirmBatchRead( + batchId: BatchId, + removalReason: RemovalReason, + deleteBatch: Boolean + ) { + if (deleteBatch) { + grantedPersistenceStrategy.unlockAndDelete(batchId.id) + } else { + grantedPersistenceStrategy.unlockAndKeep(batchId.id) + } + } + + @AnyThread + override fun dropAll() { + executorService.executeSafe("Data drop", internalLogger) { + grantedPersistenceStrategy.dropAll() + pendingPersistenceStrategy.dropAll() + } + } + + // endregion + + // region TrackingConsentProviderCallback + + override fun onConsentUpdated( + previousConsent: TrackingConsent, + newConsent: TrackingConsent + ) { + executorService.executeSafe("Data migration", internalLogger) { + if (previousConsent == TrackingConsent.PENDING) { + when (newConsent) { + TrackingConsent.GRANTED -> pendingPersistenceStrategy.migrateData(grantedPersistenceStrategy) + TrackingConsent.NOT_GRANTED -> pendingPersistenceStrategy.dropAll() + TrackingConsent.PENDING -> { + // Nothing to do + } + } + } + } + } + + // endregion +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/AsyncEventWriteScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/AsyncEventWriteScope.kt new file mode 100644 index 0000000000..a64f7a74fe --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/AsyncEventWriteScope.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.core.internal.utils.executeSafe +import java.util.concurrent.Executor + +internal class AsyncEventWriteScope( + private val executor: Executor, + private val writer: EventBatchWriter, + private val featureWriteLock: Any, + private val featureName: String, + private val internalLogger: InternalLogger +) : EventWriteScope { + override fun invoke(block: (EventBatchWriter) -> Unit) { + executor.executeSafe("eventWriteScopeInvoke-$featureName", internalLogger) { + // since writing may not be atomic: we can write batch data + batch metadata, there is a gap between + // getting file for writing and write op, we sync file operation with a feature-wide lock + synchronized(featureWriteLock) { + block.invoke(writer) + } + } + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/Batch.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Batch.kt similarity index 87% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/Batch.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Batch.kt index ef3e3a9574..14758087f8 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/Batch.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Batch.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.core.internal.data.file +package com.datadog.android.core.internal.persistence /** * Represent a batch of logs read from a persisted location. diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchConfirmation.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchConfirmation.kt new file mode 100644 index 0000000000..6113d3c111 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchConfirmation.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.WorkerThread + +internal interface BatchConfirmation { + @WorkerThread + fun markAsRead(deleteBatch: Boolean) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchData.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchData.kt new file mode 100644 index 0000000000..c4cbcc0289 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchData.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.storage.RawBatchEvent + +internal data class BatchData( + val id: BatchId, + val data: List, + val metadata: ByteArray? = null +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BatchData + + if (id != other.id) return false + if (data != other.data) return false + if (metadata != null) { + if (other.metadata == null) return false + if (!metadata.contentEquals(other.metadata)) return false + } else if (other.metadata != null) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + data.hashCode() + result = 31 * result + (metadata?.contentHashCode() ?: 0) + return result + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchId.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchId.kt new file mode 100644 index 0000000000..059421e5df --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchId.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import java.io.File + +internal data class BatchId( + val id: String +) { + + fun matchesFile(file: File): Boolean { + return file.extractFileId() == id + } + + companion object { + + fun fromFile(file: File): BatchId { + return BatchId(file.extractFileId()) + } + + private fun File.extractFileId(): String { + return absolutePath + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchReader.kt new file mode 100644 index 0000000000..6ebc948b51 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchReader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.WorkerThread +import com.datadog.android.api.storage.RawBatchEvent + +internal interface BatchReader { + + /** + * @return the metadata of the current readable file + */ + @WorkerThread + fun currentMetadata(): ByteArray? + + @WorkerThread + fun read(): List +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchWriteEventListener.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchWriteEventListener.kt new file mode 100644 index 0000000000..687a8a9fdf --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/BatchWriteEventListener.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +internal interface BatchWriteEventListener { + fun onWriteEvent(bytes: Long) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/ConsentAwareStorage.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/ConsentAwareStorage.kt new file mode 100644 index 0000000000..22c30bdba5 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/ConsentAwareStorage.kt @@ -0,0 +1,204 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.core.internal.data.upload.DataOkHttpUploader.Companion.HTTP_ACCEPTED +import com.datadog.android.core.internal.metrics.BenchmarkUploads +import com.datadog.android.core.internal.metrics.MetricsDispatcher +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.lengthSafe +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.privacy.TrackingConsent +import java.io.File +import java.util.Locale +import java.util.concurrent.ExecutorService + +internal class ConsentAwareStorage( + private val executorService: ExecutorService, + internal val grantedOrchestrator: FileOrchestrator, + internal val pendingOrchestrator: FileOrchestrator, + private val batchEventsReaderWriter: BatchFileReaderWriter, + private val batchMetadataReaderWriter: FileReaderWriter, + private val fileMover: FileMover, + private val internalLogger: InternalLogger, + internal val filePersistenceConfig: FilePersistenceConfig, + private val metricsDispatcher: MetricsDispatcher, + private val featureName: String, + private val benchmarkUploads: BenchmarkUploads = BenchmarkUploads() +) : Storage, BatchWriteEventListener { + + /** + * Keeps track of files currently being read. + */ + private val lockedReadBatches: MutableSet = mutableSetOf() + + private val writeLock = Any() + + /** @inheritdoc */ + @AnyThread + override fun getEventWriteScope( + datadogContext: DatadogContext + ): EventWriteScope { + val orchestrator = resolveOrchestrator(datadogContext) + // TODO RUM-9712 Put performance metric for event processing + event write measurement + if (orchestrator == null) { + return AsyncEventWriteScope(executorService, NoOpEventBatchWriter(), writeLock, featureName, internalLogger) + } + val writer = FileEventBatchWriter( + fileOrchestrator = orchestrator, + eventsWriter = batchEventsReaderWriter, + metadataReaderWriter = batchMetadataReaderWriter, + filePersistenceConfig = filePersistenceConfig, + batchWriteEventListener = this, + internalLogger = internalLogger + ) + return AsyncEventWriteScope(executorService, writer, writeLock, featureName, internalLogger) + } + + /** @inheritdoc */ + @WorkerThread + override fun readNextBatch(): BatchData? { + val (batchFile, metaFile) = synchronized(lockedReadBatches) { + val batchFile = grantedOrchestrator + .getReadableFile(lockedReadBatches.map { it.file }.toSet()) ?: return null + + val metaFile = grantedOrchestrator.getMetadataFile(batchFile) + lockedReadBatches.add(Batch(batchFile, metaFile)) + batchFile to metaFile + } + + val batchId = BatchId.fromFile(batchFile) + val batchMetadata = if (metaFile == null || !metaFile.existsSafe(internalLogger)) { + null + } else { + batchMetadataReaderWriter.readData(metaFile) + } + val batchData = batchEventsReaderWriter.readData(batchFile) + + return BatchData(id = batchId, data = batchData, metadata = batchMetadata) + } + + /** @inheritdoc */ + @WorkerThread + override fun confirmBatchRead( + batchId: BatchId, + removalReason: RemovalReason, + deleteBatch: Boolean + ) { + val batch = synchronized(lockedReadBatches) { + lockedReadBatches.firstOrNull { batchId.matchesFile(it.file) } + } ?: return + + if (deleteBatch) { + deleteBatch(batch, removalReason) + } + synchronized(lockedReadBatches) { + lockedReadBatches.remove(batch) + } + } + + /** @inheritdoc */ + @AnyThread + override fun dropAll() { + executorService.executeSafe("ConsentAwareStorage.dropAll", internalLogger) { + synchronized(lockedReadBatches) { + lockedReadBatches.forEach { + deleteBatch(it, RemovalReason.Flushed) + } + lockedReadBatches.clear() + } + arrayOf(pendingOrchestrator, grantedOrchestrator).forEach { orchestrator -> + orchestrator.getAllFiles().forEach { + val metaFile = orchestrator.getMetadataFile(it) + deleteBatch(it, metaFile, RemovalReason.Flushed) + } + } + } + } + + override fun onWriteEvent(bytes: Long) { + benchmarkUploads.sendBenchmarkBytesWritten( + featureName = featureName, + value = bytes + ) + } + + @AnyThread + private fun resolveOrchestrator(datadogContext: DatadogContext): FileOrchestrator? { + return when (datadogContext.trackingConsent) { + TrackingConsent.GRANTED -> grantedOrchestrator + TrackingConsent.PENDING -> pendingOrchestrator + TrackingConsent.NOT_GRANTED -> null + } + } + + @WorkerThread + private fun deleteBatch(batch: Batch, reason: RemovalReason) { + deleteBatch(batch.file, batch.metaFile, reason) + } + + @WorkerThread + private fun deleteBatch(batchFile: File, metaFile: File?, reason: RemovalReason) { + deleteBatchFile(batchFile, reason) + if (metaFile?.existsSafe(internalLogger) == true) { + deleteBatchMetadataFile(metaFile) + } + } + + @WorkerThread + private fun deleteBatchFile(batchFile: File, reason: RemovalReason) { + val fileSizeBeforeDeletion = batchFile.lengthSafe(internalLogger) + + val result = fileMover.delete(batchFile) + if (result) { + val numPendingFiles = grantedOrchestrator.decrementAndGetPendingFilesCount() + metricsDispatcher.sendBatchDeletedMetric(batchFile, reason, numPendingFiles) + + if (reason == RemovalReason.IntakeCode(HTTP_ACCEPTED) && fileSizeBeforeDeletion > 0) { + benchmarkUploads.sendBenchmarkBytesDeleted( + featureName = featureName, + value = fileSizeBeforeDeletion + ) + } + } else { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { WARNING_DELETE_FAILED.format(Locale.US, batchFile.path) } + ) + } + } + + @WorkerThread + private fun deleteBatchMetadataFile(metadataFile: File) { + val result = fileMover.delete(metadataFile) + if (!result) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { WARNING_DELETE_FAILED.format(Locale.US, metadataFile.path) } + ) + } + } + + private data class Batch(val file: File, val metaFile: File?) + + companion object { + internal const val WARNING_DELETE_FAILED = "Unable to delete file: %s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/DataReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/DataReader.kt new file mode 100644 index 0000000000..a92b521d0a --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/DataReader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.WorkerThread +import com.datadog.tools.annotation.NoOpImplementation + +/** + * A class able to read data from a persistence location (e.g. file, database, …). + */ +@NoOpImplementation +internal interface DataReader { + + /** + * Reads the next piece of data and lock it so that it can't be read or written to by anyone. + */ + @WorkerThread + fun lockAndReadNext(): Batch? + + /** + * Marks the data as read and releases it to be read/written to by someone else. + */ + @WorkerThread + fun release(data: Batch) + + /** + * Marks the data as read and deletes it. + */ + @WorkerThread + fun drop(data: Batch) + + /** + * Drop all available data. + */ + @WorkerThread + fun dropAll() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/DataWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/DataWriter.kt new file mode 100644 index 0000000000..9831566b55 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/DataWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.WorkerThread +import com.datadog.tools.annotation.NoOpImplementation + +/** + * A class able to write a data (or list of data) with type [T] in a persistence location + * (e.g. file, database, …). + * @param T the type of data to store + */ +@NoOpImplementation +internal interface DataWriter { + + /** + * Writes the element into the relevant location. + */ + @WorkerThread + fun write(element: T) + + /** + * Writes a list of elements into the relevant location. + */ + @WorkerThread + fun write(data: List) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Deserializer.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Deserializer.kt new file mode 100644 index 0000000000..ebd92cf38e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Deserializer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.lint.InternalApi + +/** + * The Deserializer generic interface. Should be implemented by any custom deserializer. + * + * FOR INTERNAL USAGE ONLY. THIS INTERFACE CONTENT MAY CHANGE WITHOUT NOTICE. + */ +@InternalApi +interface Deserializer

{ + + /** + * Deserializes the data from the given payload type into given output type. + * @return the model represented by the given payload, or null when deserialization + * is impossible. + */ + @InternalApi + fun deserialize(model: P): R? +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/FileEventBatchWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/FileEventBatchWriter.kt new file mode 100644 index 0000000000..7abbc1eeb3 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/FileEventBatchWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.FileWriter +import com.datadog.android.core.internal.persistence.file.existsSafe +import java.io.File +import java.util.Locale + +internal class FileEventBatchWriter( + private val fileOrchestrator: FileOrchestrator, + private val eventsWriter: FileWriter, + private val metadataReaderWriter: FileReaderWriter, + private val filePersistenceConfig: FilePersistenceConfig, + private val batchWriteEventListener: BatchWriteEventListener, + private val internalLogger: InternalLogger +) : EventBatchWriter { + + @get:WorkerThread + private val batchFile: File? by lazy { + @Suppress("ThreadSafety") // called in the worker context + fileOrchestrator.getWritableFile() + } + + @get:WorkerThread + private val metadataFile: File? + get() = batchFile?.let { + @Suppress("ThreadSafety") // called in the worker context + fileOrchestrator.getMetadataFile(it) + } + + @WorkerThread + override fun currentMetadata(): ByteArray? { + return with(metadataFile) { + if (this == null || !existsSafe(internalLogger)) { + null + } else { + metadataReaderWriter.readData(this) + } + } + } + + @WorkerThread + override fun write( + event: RawBatchEvent, + batchMetadata: ByteArray?, + eventType: EventType + ): Boolean { + val (batchFile, metadataFile) = batchFile to metadataFile + if (batchFile == null) { + internalLogger.log( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { NO_BATCH_FILE_AVAILABLE } + ) + return false + } + + // prevent useless operation for empty event + return if (event.data.isEmpty()) { + true + } else if (!checkEventSize(event.data.size)) { + false + } else if (eventsWriter.writeData(batchFile, event, true)) { + batchWriteEventListener.onWriteEvent(event.data.size.toLong()) + if (batchMetadata?.isNotEmpty() == true && metadataFile != null) { + writeBatchMetadata(metadataFile, batchMetadata) + } + true + } else { + false + } + } + + private fun checkEventSize(eventSize: Int): Boolean { + if (eventSize > filePersistenceConfig.maxItemSize) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { + ERROR_LARGE_DATA.format( + Locale.US, + eventSize, + filePersistenceConfig.maxItemSize + ) + } + ) + return false + } + return true + } + + @WorkerThread + private fun writeBatchMetadata(metadataFile: File, metadata: ByteArray) { + val result = metadataReaderWriter.writeData( + metadataFile, + metadata, + false + ) + if (!result) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { + WARNING_METADATA_WRITE_FAILED.format( + Locale.US, + metadataFile.path + ) + } + ) + } + } + + companion object { + internal const val WARNING_METADATA_WRITE_FAILED = "Unable to write metadata file: %s" + internal const val ERROR_LARGE_DATA = "Can't write data with size %d (max item size is %d)" + internal const val NO_BATCH_FILE_AVAILABLE = "No batch file available" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/JsonObjectDeserializer.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/JsonObjectDeserializer.kt new file mode 100644 index 0000000000..6fe2e48faf --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/JsonObjectDeserializer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.InternalLogger +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.util.Locale + +internal class JsonObjectDeserializer(private val internalLogger: InternalLogger) : + Deserializer { + override fun deserialize(model: String): JsonObject? { + return try { + JsonParser.parseString(model).asJsonObject + } catch (jpe: JsonParseException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { DESERIALIZE_ERROR_MESSAGE_FORMAT.format(Locale.US, model) }, + jpe + ) + null + } catch (ise: IllegalStateException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { DESERIALIZE_ERROR_MESSAGE_FORMAT.format(Locale.US, model) }, + ise + ) + null + } + } + + companion object { + const val DESERIALIZE_ERROR_MESSAGE_FORMAT = + "Error while trying to deserialize the RumEvent: %s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/NoOpEventBatchWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/NoOpEventBatchWriter.kt new file mode 100644 index 0000000000..ced7db7837 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/NoOpEventBatchWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent + +internal class NoOpEventBatchWriter : EventBatchWriter { + + override fun currentMetadata(): ByteArray? { + return null + } + + override fun write( + event: RawBatchEvent, + batchMetadata: ByteArray?, + eventType: EventType + ): Boolean = true +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/PayloadDecoration.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/PayloadDecoration.kt new file mode 100644 index 0000000000..379b89dd1e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/PayloadDecoration.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +internal data class PayloadDecoration( + val prefix: CharSequence, + val suffix: CharSequence, + val separator: CharSequence +) { + + val separatorBytes = separator.toString().toByteArray(Charsets.UTF_8) + val prefixBytes = prefix.toString().toByteArray(Charsets.UTF_8) + val suffixBytes = suffix.toString().toByteArray(Charsets.UTF_8) + + companion object { + val JSON_ARRAY_DECORATION = PayloadDecoration("[", "]", ",") + val NEW_LINE_DECORATION = PayloadDecoration("", "", "\n") + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Storage.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Storage.kt new file mode 100644 index 0000000000..b0b4a88d26 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/Storage.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Main core api to interact with the local storage of events. + */ +@NoOpImplementation +internal interface Storage { + + /** + * Utility to get the scope for the writing operation, synchronously. + * @param datadogContext the context for the write operation + */ + @AnyThread + fun getEventWriteScope( + datadogContext: DatadogContext + ): EventWriteScope + + /** + * Utility to read a batch, synchronously. + */ + @WorkerThread + fun readNextBatch(): BatchData? + + /** + * Utility to update the state of a batch, synchronously. + * @param batchId the id of the Batch to confirm + * @param removalReason the reason why the batch is being removed + * @param deleteBatch if `true` the batch will be deleted, otherwise it will be marked as + * not readable. + */ + @WorkerThread + fun confirmBatchRead( + batchId: BatchId, + removalReason: RemovalReason, + deleteBatch: Boolean + ) + + /** + * Removes all the files backed by this storage, synchronously. + */ + @AnyThread + fun dropAll() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt new file mode 100644 index 0000000000..4ef74f1acb --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.api.storage.datastore.DataStoreReadCallback +import com.datadog.android.api.storage.datastore.DataStoreWriteCallback +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.core.persistence.Serializer +import java.util.concurrent.ExecutorService + +internal class DataStoreFileHandler( + private val executorService: ExecutorService, + private val internalLogger: InternalLogger, + private val dataStoreFileReader: DatastoreFileReader, + private val datastoreFileWriter: DatastoreFileWriter +) : DataStoreHandler { + + override fun setValue( + key: String, + data: T, + version: Int, + callback: DataStoreWriteCallback?, + serializer: Serializer + ) { + executorService.executeSafe("dataStoreWrite", internalLogger) { + datastoreFileWriter.write(key, data, serializer, callback, version) + } + } + + override fun removeValue(key: String, callback: DataStoreWriteCallback?) { + executorService.executeSafe("dataStoreRemove", internalLogger) { + datastoreFileWriter.delete(key, callback) + } + } + + override fun clearAllData() { + executorService.executeSafe("dataStoreClearAllData", internalLogger) { + datastoreFileWriter.clearAllData() + } + } + + override fun value( + key: String, + version: Int?, + callback: DataStoreReadCallback, + deserializer: Deserializer + ) { + executorService.executeSafe("dataStoreRead", internalLogger) { + dataStoreFileReader.read(key, deserializer, version, callback) + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt new file mode 100644 index 0000000000..1096b1e778 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.mkdirsSafe +import java.io.File +import java.util.Locale + +internal class DataStoreFileHelper( + private val internalLogger: InternalLogger +) { + internal fun getDataStoreFile( + storageDir: File, + featureName: String, + key: String + ): File { + val dataStoreDirectory = getDataStoreDirectory( + featureName = featureName, + storageDir = storageDir + ) + + return File(dataStoreDirectory, key) + } + + internal fun getDataStoreDirectory( + storageDir: File, + featureName: String + ): File { + val folderName = DATASTORE_FOLDER_NAME.format( + Locale.US, + DataStoreHandler.CURRENT_DATASTORE_VERSION + ) + + val dataStoreDirectory = File( + File(storageDir, folderName), + featureName + ) + + if (!dataStoreDirectory.existsSafe(internalLogger)) { + dataStoreDirectory.mkdirsSafe(internalLogger) + } + + return dataStoreDirectory + } + + internal companion object { + internal const val DATASTORE_FOLDER_NAME = "datastore_v%s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt new file mode 100644 index 0000000000..16292e01e6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreReadCallback +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.internal.utils.toInt +import com.datadog.android.core.persistence.datastore.DataStoreContent +import java.io.File +import java.util.Locale + +internal class DatastoreFileReader( + private val dataStoreFileHelper: DataStoreFileHelper, + private val featureName: String, + private val storageDir: File, + private val internalLogger: InternalLogger, + private val tlvBlockFileReader: TLVBlockFileReader +) { + @WorkerThread + internal fun read( + key: String, + deserializer: Deserializer, + version: Int? = null, + callback: DataStoreReadCallback + ) { + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + storageDir = storageDir, + featureName = featureName, + key = key + ) + + if (!datastoreFile.existsSafe(internalLogger)) { + callback.onSuccess(null) + return + } + + readFromDataStoreFile(datastoreFile, deserializer, tlvBlockFileReader, version, callback) + } + + @Suppress("ReturnCount", "ThreadSafety") + private fun readFromDataStoreFile( + datastoreFile: File, + deserializer: Deserializer, + tlvBlockFileReader: TLVBlockFileReader, + requestedVersion: Int?, + callback: DataStoreReadCallback + ) { + val tlvBlocks = tlvBlockFileReader.read(datastoreFile) + + // there should be as many blocks read as there are block types + val numberBlocksFound = tlvBlocks.size + val numberBlocksExpected = TLVBlockType.values().size + if (numberBlocksFound != numberBlocksExpected) { + logInvalidNumberOfBlocksError(numberBlocksFound, numberBlocksExpected) + callback.onFailure() + return + } + + val dataStoreContent = mapToDataStoreContents(deserializer, tlvBlocks) + + if (dataStoreContent == null) { + callback.onFailure() + return + } + + // if an optional version is specified then only return data if the entry version exactly matches + if (requestedVersion != null && requestedVersion != dataStoreContent.versionCode) { + callback.onSuccess(null) + return + } + + callback.onSuccess(dataStoreContent) + } + + private fun mapToDataStoreContents( + deserializer: Deserializer, + tlvBlocks: List + ): DataStoreContent? { + if (tlvBlocks[0].type != TLVBlockType.VERSION_CODE && + tlvBlocks[1].type != TLVBlockType.DATA + ) { + logBlocksInUnexpectedBlocksOrderError() + return null + } + + val versionCodeBlock = tlvBlocks[0] + val dataBlock = tlvBlocks[1] + + return DataStoreContent( + versionCode = versionCodeBlock.data.toInt(), + data = deserializer.deserialize(String(dataBlock.data)) + ) + } + + private fun logInvalidNumberOfBlocksError(numberBlocksFound: Int, numberBlocksExpected: Int) { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { + INVALID_NUMBER_OF_BLOCKS_ERROR + .format(Locale.US, numberBlocksFound, numberBlocksExpected) + } + ) + } + + private fun logBlocksInUnexpectedBlocksOrderError() { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { UNEXPECTED_BLOCKS_ORDER_ERROR } + ) + } + + internal companion object { + internal const val INVALID_NUMBER_OF_BLOCKS_ERROR = + "Read error - datastore entry has invalid number of blocks. Was: %d, expected: %d" + internal const val UNEXPECTED_BLOCKS_ORDER_ERROR = + "Read error - blocks are in an unexpected order" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt new file mode 100644 index 0000000000..466f71523c --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreWriteCallback +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.deleteDirectoryContentsSafe +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.internal.utils.join +import com.datadog.android.core.internal.utils.toByteArray +import com.datadog.android.core.persistence.Serializer +import java.io.File + +internal class DatastoreFileWriter( + private val dataStoreFileHelper: DataStoreFileHelper, + private val featureName: String, + private val storageDir: File, + private val internalLogger: InternalLogger, + private val fileReaderWriter: FileReaderWriter +) { + @WorkerThread + internal fun write( + key: String, + data: T, + serializer: Serializer, + callback: DataStoreWriteCallback?, + version: Int + ) { + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + storageDir = storageDir, + featureName = featureName, + key = key + ) + + val versionCodeBlock = getVersionCodeBlock(version) + val dataBlock = getDataBlock(data, serializer) + + // failed to serialize one or more blocks + if (versionCodeBlock == null || dataBlock == null) { + callback?.onFailure() + return + } + + val dataToWrite = listOf(versionCodeBlock, dataBlock).join( + separator = EMPTY_BYTE_ARRAY, + internalLogger = internalLogger + ) + + val result = fileReaderWriter.writeData( + file = datastoreFile, + data = dataToWrite, + append = false + ) + + if (result) { + callback?.onSuccess() + } else { + callback?.onFailure() + } + } + + @WorkerThread + internal fun delete(key: String, callback: DataStoreWriteCallback?) { + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + storageDir = storageDir, + featureName = featureName, + key = key + ) + + if (datastoreFile.existsSafe(internalLogger)) { + val result = datastoreFile.deleteSafe(internalLogger) + if (result) { + callback?.onSuccess() + } else { + callback?.onFailure() + } + } + } + + @WorkerThread + internal fun clearAllData() { + val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( + featureName = featureName, + storageDir = storageDir + ) + + if (dataStoreDirectory.existsSafe(internalLogger)) { + dataStoreDirectory.deleteDirectoryContentsSafe(internalLogger) + } + } + + private fun getDataBlock( + data: T, + serializer: Serializer + ): ByteArray? { + val serializedData = serializer.serialize(data)?.toByteArray() + + if (serializedData == null) { + logFailedToSerializeDataError() + return null + } + + val dataBlock = TLVBlock( + type = TLVBlockType.DATA, + data = serializedData, + internalLogger = internalLogger + ) + + return dataBlock.serialize() + } + + private fun getVersionCodeBlock(version: Int): ByteArray? { + val versionCodeByteArray = version.toByteArray() + val versionBlock = TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = versionCodeByteArray, + internalLogger = internalLogger + ) + + return versionBlock.serialize() + } + + private fun logFailedToSerializeDataError() { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + messageBuilder = { FAILED_TO_SERIALIZE_DATA_ERROR } + ) + } + + internal companion object { + internal const val FAILED_TO_SERIALIZE_DATA_ERROR = + "Write error - Failed to serialize data for the datastore" + private val EMPTY_BYTE_ARRAY = ByteArray(0) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt new file mode 100644 index 0000000000..8cbd98cfd9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.api.storage.datastore.DataStoreReadCallback +import com.datadog.android.api.storage.datastore.DataStoreWriteCallback +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.persistence.Serializer + +internal class NoOpDataStoreHandler : DataStoreHandler { + override fun setValue( + key: String, + data: T, + version: Int, + callback: DataStoreWriteCallback?, + serializer: Serializer + ) { + // NoOp Implementation + } + + override fun value( + key: String, + version: Int?, + callback: DataStoreReadCallback, + deserializer: Deserializer + ) { + // NoOp Implementation + } + + override fun removeValue( + key: String, + callback: DataStoreWriteCallback? + ) { + // NoOp Implementation + } + + override fun clearAllData() { + // NoOp Implementation + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/EncryptedFileReaderWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/EncryptedFileReaderWriter.kt new file mode 100644 index 0000000000..42da4f529b --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/EncryptedFileReaderWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.security.Encryption +import java.io.File + +internal class EncryptedFileReaderWriter( + internal val encryption: Encryption, + internal val delegate: FileReaderWriter, + private val internalLogger: InternalLogger +) : FileReaderWriter by delegate { + + @Suppress("ReturnCount") + @WorkerThread + override fun writeData( + file: File, + data: ByteArray, + append: Boolean + ): Boolean { + if (append) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { APPEND_MODE_NOT_SUPPORTED_MESSAGE } + ) + return false + } + + val encryptedData = encryption.encrypt(data) + + if (data.isNotEmpty() && encryptedData.isEmpty()) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { BAD_ENCRYPTION_RESULT_MESSAGE } + ) + return false + } + + return delegate.writeData( + file, + encryptedData, + append + ) + } + + @WorkerThread + override fun readData( + file: File + ): ByteArray { + return encryption.decrypt(delegate.readData(file)) + } + + companion object { + internal const val BAD_ENCRYPTION_RESULT_MESSAGE = "Encryption of non-empty data produced" + + " empty result, aborting write operation." + internal const val APPEND_MODE_NOT_SUPPORTED_MESSAGE = "Append mode is not supported," + + " use EncryptedBatchFileReaderWriter instead." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt new file mode 100644 index 0000000000..0b47b58d73 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt @@ -0,0 +1,215 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +@file:Suppress("TooManyFunctions") + +package com.datadog.android.core.internal.persistence.file + +import com.datadog.android.api.InternalLogger +import com.datadog.android.lint.InternalApi +import java.io.File +import java.io.FileFilter +import java.io.FilenameFilter +import java.nio.charset.Charset + +/* + * The java.lang.File class throws a SecurityException for the following calls: + * - canRead() + * - canWrite() + * - delete() + * - exists() + * - isFile() + * - isDir() + * - listFiles(…) + * - length() + * The following set of extension make sure that every call to those methods + * is safeguarded to avoid crashing the customer's app. + */ + +@Suppress("TooGenericExceptionCaught") +private fun File.safeCall( + default: T, + internalLogger: InternalLogger, + lambda: File.() -> T +): T { + return try { + lambda() + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { "Security exception was thrown for file ${this.path}" }, + e + ) + default + } catch (e: Exception) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { "Unexpected exception was thrown for file ${this.path}" }, + e + ) + default + } +} + +internal fun File.canWriteSafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + canWrite() + } +} + +/** + * Non-throwing version of [File.canRead]. If exception happens, false is returned. + */ +@InternalApi +fun File.canReadSafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + canRead() + } +} + +internal fun File.deleteSafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + delete() + } +} + +/** + * Non-throwing version of [File.exists]. If exception happens, false is returned. + */ +@InternalApi +fun File.existsSafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + exists() + } +} + +internal fun File.isFileSafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + isFile() + } +} + +internal fun File.isDirectorySafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + isDirectory() + } +} + +internal fun File.listFilesSafe(internalLogger: InternalLogger): Array? { + return safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + listFiles() + } +} + +internal fun File.listFilesSafe(filter: FileFilter, internalLogger: InternalLogger): Array? { + return safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + listFiles(filter) + } +} + +/** + * Non-throwing version of [File.listFiles]. If exception happens, null is returned. + */ +@InternalApi +fun File.listFilesSafe(internalLogger: InternalLogger, filter: FilenameFilter): Array? { + return safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + listFiles(filter) + } +} + +internal fun File.lengthSafe(internalLogger: InternalLogger): Long { + return safeCall(default = 0L, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + length() + } +} + +internal fun File.mkdirsSafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + mkdirs() + } +} + +internal fun File.renameToSafe(dest: File, internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + renameTo(dest) + } +} + +internal fun File.deleteDirectoryContentsSafe(internalLogger: InternalLogger) { + this.listFilesSafe(internalLogger)?.forEach { + it.deleteSafe(internalLogger) + } +} + +/** + * Non-throwing version of [File.readText]. If exception happens, null is returned. + */ +@InternalApi +fun File.readTextSafe(charset: Charset = Charsets.UTF_8, internalLogger: InternalLogger): String? { + return if (existsSafe(internalLogger) && canReadSafe(internalLogger)) { + safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + readText(charset) + } + } else { + null + } +} + +internal fun File.readBytesSafe(internalLogger: InternalLogger): ByteArray? { + return if (existsSafe(internalLogger) && canReadSafe(internalLogger)) { + safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + readBytes() + } + } else { + null + } +} + +/** + * Non-throwing version of [File.readLines]. If exception happens, null is returned. + */ +@InternalApi +fun File.readLinesSafe( + charset: Charset = Charsets.UTF_8, + internalLogger: InternalLogger +): List? { + return if (existsSafe(internalLogger) && canReadSafe(internalLogger)) { + safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + readLines(charset) + } + } else { + null + } +} + +internal fun File.writeTextSafe( + text: String, + charset: Charset = Charsets.UTF_8, + internalLogger: InternalLogger +) { + if (existsSafe(internalLogger) && canWriteSafe(internalLogger)) { + safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + writeText(text, charset) + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileMover.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileMover.kt new file mode 100644 index 0000000000..517ef932b0 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileMover.kt @@ -0,0 +1,107 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import java.io.File +import java.io.FileNotFoundException +import java.util.Locale + +internal class FileMover(val internalLogger: InternalLogger) { + + /** + * Deletes the file or directory (recursively if needed). + * @param target the target [File] to delete + * @return whether the delete was successful + */ + @WorkerThread + fun delete(target: File): Boolean { + return try { + target.deleteRecursively() + } catch (e: FileNotFoundException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_DELETE.format(Locale.US, target.path) }, + e + ) + false + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_DELETE.format(Locale.US, target.path) }, + e + ) + false + } + } + + /** + * Move the children files from `srcDir` to the `destDir`. + */ + @Suppress("ReturnCount") + @WorkerThread + fun moveFiles(srcDir: File, destDir: File): Boolean { + if (!srcDir.existsSafe(internalLogger)) { + internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.MAINTAINER, + { INFO_MOVE_NO_SRC.format(Locale.US, srcDir.path) } + ) + return true + } + if (!srcDir.isDirectorySafe(internalLogger)) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_MOVE_NOT_DIR.format(Locale.US, srcDir.path) } + ) + return false + } + if (!destDir.existsSafe(internalLogger)) { + if (!destDir.mkdirsSafe(internalLogger)) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { ERROR_MOVE_NO_DST.format(Locale.US, srcDir.path) } + ) + return false + } + } else if (!destDir.isDirectorySafe(internalLogger)) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_MOVE_NOT_DIR.format(Locale.US, destDir.path) } + ) + return false + } + + val srcFiles = srcDir.listFilesSafe(internalLogger).orEmpty() + return srcFiles.all { file -> moveFile(file, destDir) } + } + + private fun moveFile(file: File, destDir: File): Boolean { + val destFile = File(destDir, file.name) + return file.renameToSafe(destFile, internalLogger) + } + + @Suppress("StringLiteralDuplication") + companion object { + internal const val ERROR_DELETE = "Unable to delete file: %s" + internal const val INFO_MOVE_NO_SRC = "Unable to move files; " + + "source directory does not exist: %s" + internal const val ERROR_MOVE_NOT_DIR = "Unable to move files; " + + "file is not a directory: %s" + internal const val ERROR_MOVE_NO_DST = "Unable to move files; " + + "could not create directory: %s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileOrchestrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileOrchestrator.kt new file mode 100644 index 0000000000..dd9e896938 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileOrchestrator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import androidx.annotation.WorkerThread +import com.datadog.tools.annotation.NoOpImplementation +import java.io.File + +/** + * A class that will manage set of files that can be read/written to. + * + * The contract of this class is that: + * - a File that can be written to cannot be read; + * - a File that can be read cannot be written to; + */ +@NoOpImplementation +internal interface FileOrchestrator { + + /** + * @return a File with enough space to write data, or null if no space is available + * or the disk can't be written to. + */ + @WorkerThread + fun getWritableFile(): File? + + /** + * @param excludeFiles a set of files to exclude from the readable files + * @return a File that can be read from, or null is no file is available yet. + */ + @WorkerThread + fun getReadableFile(excludeFiles: Set): File? + + /** + * @return a List of all flushable files. A flushable file is any file (readable or writable) + * which contains valid data and is ready to be uploaded to the events endpoint. + */ + @WorkerThread + fun getFlushableFiles(): List + + /** + * @return a list of files in this orchestrator (both writable and readable) + */ + @WorkerThread + fun getAllFiles(): List + + /** + * @return the root directory of this orchestrator, or null if the root directory is not + * available (e.g.: because of a SecurityException) + */ + @WorkerThread + fun getRootDir(): File? + + /** + * @return the metadata file for a given file, or null if there is no such. + */ + @WorkerThread + fun getMetadataFile(file: File): File? + + /** + * @return the name of the root directory of this orchestrator or null if the root directory does not exist. + */ + fun getRootDirName(): String? + + /** + * @return the number of pending files in the orchestrator, after decrementing by 1. + */ + fun decrementAndGetPendingFilesCount(): Int +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FilePersistenceConfig.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FilePersistenceConfig.kt new file mode 100644 index 0000000000..e363b85ac6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FilePersistenceConfig.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +/** + * Limits are defined at https://docs.datadoghq.com/api/latest/logs/#send-logs + */ +internal data class FilePersistenceConfig( + val recentDelayMs: Long = MAX_DELAY_BETWEEN_MESSAGES_MS, + val maxBatchSize: Long = MAX_BATCH_SIZE, + val maxItemSize: Long = MAX_ITEM_SIZE, + val maxItemsPerBatch: Int = MAX_ITEMS_PER_BATCH, + val oldFileThreshold: Long = OLD_FILE_THRESHOLD, + val maxDiskSpace: Long = MAX_DISK_SPACE, + val cleanupFrequencyThreshold: Long = CLEANUP_FREQUENCY_THRESHOLD_MS +) { + companion object { + internal const val MAX_BATCH_SIZE: Long = 4L * 1024 * 1024 // 4 MB + internal const val MAX_ITEMS_PER_BATCH: Int = 500 + internal const val MAX_ITEM_SIZE: Long = 512L * 1024 // 512 KB + internal const val OLD_FILE_THRESHOLD: Long = 18L * 60L * 60L * 1000L // 18 hours + internal const val MAX_DISK_SPACE: Long = 128 * MAX_BATCH_SIZE // 512 MB + internal const val MAX_DELAY_BETWEEN_MESSAGES_MS = 5000L + internal const val CLEANUP_FREQUENCY_THRESHOLD_MS = 5000L // 5s + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileReader.kt new file mode 100644 index 0000000000..95135d2619 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileReader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import androidx.annotation.WorkerThread +import java.io.File + +internal interface FileReader { + + /** + * Reads data from the given file. + * @param file the file to read from + * @return the data + */ + @WorkerThread + fun readData(file: File): T +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileReaderWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileReaderWriter.kt new file mode 100644 index 0000000000..b808785a13 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileReaderWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import com.datadog.android.api.InternalLogger +import com.datadog.android.security.Encryption + +internal interface FileReaderWriter : FileWriter, FileReader { + companion object { + + /** + * Creates either plain [PlainFileReaderWriter] or [PlainFileReaderWriter] wrapped in + * [EncryptedFileReaderWriter] if encryption is provided. + */ + fun create(internalLogger: InternalLogger, encryption: Encryption?): FileReaderWriter { + val readerWriter = PlainFileReaderWriter(internalLogger) + return if (encryption == null) { + readerWriter + } else { + EncryptedFileReaderWriter( + encryption, + readerWriter, + internalLogger + ) + } + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileWriter.kt new file mode 100644 index 0000000000..2ba46bf595 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import androidx.annotation.WorkerThread +import java.io.File + +internal interface FileWriter { + + /** + * Writes data as a [T] into a file. + * @type T type of the data to write + * @param file the file to write to + * @param data the data to write + * @param append whether to append data at the end of the file or overwrite + * @return whether the write operation was successful + */ + @WorkerThread + fun writeData( + file: File, + data: T, + append: Boolean + ): Boolean +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/PlainFileReaderWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/PlainFileReaderWriter.kt new file mode 100644 index 0000000000..dbeec361da --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/PlainFileReaderWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.utils.use +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.Locale + +/** + * Stores data as-is. Use for any non-RUM/Trace/Logs data. + */ +internal class PlainFileReaderWriter( + private val internalLogger: InternalLogger +) : FileReaderWriter { + + // region FileWriter+FileReader + + @WorkerThread + override fun writeData( + file: File, + data: ByteArray, + append: Boolean + ): Boolean { + return try { + lockFileAndWriteData(file, append, data) + true + } catch (e: IOException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_WRITE.format(Locale.US, file.path) }, + e + ) + false + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_WRITE.format(Locale.US, file.path) }, + e + ) + false + } + } + + @WorkerThread + override fun readData( + file: File + ): ByteArray { + return try { + if (!file.exists()) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_READ.format(Locale.US, file.path) } + ) + EMPTY_BYTE_ARRAY + } else if (file.isDirectory) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_READ.format(Locale.US, file.path) } + ) + EMPTY_BYTE_ARRAY + } else { + @Suppress("UnsafeThirdPartyFunctionCall") // necessary catch blocks exist + file.readBytes() + } + } catch (e: IOException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_READ.format(Locale.US, file.path) }, + e + ) + EMPTY_BYTE_ARRAY + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_READ.format(Locale.US, file.path) }, + e + ) + EMPTY_BYTE_ARRAY + } + } + + // endregion + + // region Internal + + @Throws(IOException::class) + @Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block + private fun lockFileAndWriteData( + file: File, + append: Boolean, + data: ByteArray + ) { + FileOutputStream(file, append).use { outputStream -> + outputStream.channel.lock().use { + outputStream.write(data) + } + } + } + + // endregion + + companion object { + + private val EMPTY_BYTE_ARRAY = ByteArray(0) + + internal const val ERROR_WRITE = "Unable to write data to file: %s" + internal const val ERROR_READ = "Unable to read data from file: %s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileMigrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileMigrator.kt new file mode 100644 index 0000000000..a33dc71bee --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileMigrator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.privacy.TrackingConsent + +internal class ConsentAwareFileMigrator( + private val fileMover: FileMover, + private val internalLogger: InternalLogger +) : DataMigrator { + + @WorkerThread + override fun migrateData( + previousState: TrackingConsent?, + previousFileOrchestrator: FileOrchestrator, + newState: TrackingConsent, + newFileOrchestrator: FileOrchestrator + ) { + val operation = resolveMigrationOperation( + previousState, + newState, + previousFileOrchestrator, + newFileOrchestrator + ) + operation.run() + } + + @WorkerThread + private fun resolveMigrationOperation( + previousState: TrackingConsent?, + newState: TrackingConsent, + previousFileOrchestrator: FileOrchestrator, + newFileOrchestrator: FileOrchestrator + ) = when (previousState to newState) { + null to TrackingConsent.PENDING, + null to TrackingConsent.GRANTED, + null to TrackingConsent.NOT_GRANTED, + TrackingConsent.PENDING to TrackingConsent.NOT_GRANTED -> { + WipeDataMigrationOperation( + previousFileOrchestrator.getRootDir(), + fileMover, + internalLogger + ) + } + + TrackingConsent.GRANTED to TrackingConsent.PENDING, + TrackingConsent.NOT_GRANTED to TrackingConsent.PENDING -> { + WipeDataMigrationOperation( + newFileOrchestrator.getRootDir(), + fileMover, + internalLogger + ) + } + + TrackingConsent.PENDING to TrackingConsent.GRANTED -> { + MoveDataMigrationOperation( + previousFileOrchestrator.getRootDir(), + newFileOrchestrator.getRootDir(), + fileMover, + internalLogger + ) + } + + TrackingConsent.PENDING to TrackingConsent.PENDING, + TrackingConsent.GRANTED to TrackingConsent.GRANTED, + TrackingConsent.GRANTED to TrackingConsent.NOT_GRANTED, + TrackingConsent.NOT_GRANTED to TrackingConsent.NOT_GRANTED, + TrackingConsent.NOT_GRANTED to TrackingConsent.GRANTED -> { + NoOpDataMigrationOperation() + } + + else -> { + internalLogger.log( + InternalLogger.Level.WARN, + listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { "Unexpected consent migration from $previousState to $newState" } + ) + NoOpDataMigrationOperation() + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileOrchestrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileOrchestrator.kt new file mode 100644 index 0000000000..0941b0afc1 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileOrchestrator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.privacy.TrackingConsentProviderCallback +import java.io.File +import java.util.concurrent.ExecutorService + +internal open class ConsentAwareFileOrchestrator( + consentProvider: ConsentProvider, + internal val pendingOrchestrator: FileOrchestrator, + internal val grantedOrchestrator: FileOrchestrator, + internal val dataMigrator: DataMigrator, + internal val executorService: ExecutorService, + internal val internalLogger: InternalLogger +) : FileOrchestrator, TrackingConsentProviderCallback { + + @Volatile + private lateinit var delegateOrchestrator: FileOrchestrator + + init { + handleConsentChange(null, consentProvider.getConsent()) + @Suppress("LeakingThis") + consentProvider.registerCallback(this) + } + + // region FileOrchestrator + + @WorkerThread + override fun getWritableFile(): File? { + return delegateOrchestrator.getWritableFile() + } + + @WorkerThread + override fun getReadableFile(excludeFiles: Set): File? { + return grantedOrchestrator.getReadableFile(excludeFiles) + } + + @WorkerThread + override fun getAllFiles(): List { + return pendingOrchestrator.getAllFiles() + grantedOrchestrator.getAllFiles() + } + + @WorkerThread + override fun getRootDir(): File? { + return null + } + + override fun getRootDirName(): String? { + return null + } + + @WorkerThread + override fun getFlushableFiles(): List { + return grantedOrchestrator.getFlushableFiles() + } + + @WorkerThread + override fun getMetadataFile(file: File): File? { + return delegateOrchestrator.getMetadataFile(file) + } + + override fun decrementAndGetPendingFilesCount(): Int { + return delegateOrchestrator.decrementAndGetPendingFilesCount() + } + + // endregion + + // region TrackingConsentProviderCallback + + override fun onConsentUpdated( + previousConsent: TrackingConsent, + newConsent: TrackingConsent + ) { + handleConsentChange(previousConsent, newConsent) + } + + // endregion + + // region Internal + + @AnyThread + private fun handleConsentChange( + previousConsent: TrackingConsent?, + newConsent: TrackingConsent + ) { + val previousOrchestrator = resolveDelegateOrchestrator(previousConsent) + val newOrchestrator = resolveDelegateOrchestrator(newConsent) + executorService.executeSafe("Data migration", internalLogger) { + dataMigrator.migrateData( + previousConsent, + previousOrchestrator, + newConsent, + newOrchestrator + ) + delegateOrchestrator = newOrchestrator + } + } + + private fun resolveDelegateOrchestrator(consent: TrackingConsent?): FileOrchestrator { + return when (consent) { + TrackingConsent.PENDING, null -> pendingOrchestrator + TrackingConsent.GRANTED -> grantedOrchestrator + TrackingConsent.NOT_GRANTED -> NO_OP_ORCHESTRATOR + } + } + + // endregion + + companion object { + internal val NO_OP_ORCHESTRATOR: FileOrchestrator = NoOpFileOrchestrator() + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/DataMigrationOperation.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/DataMigrationOperation.kt new file mode 100644 index 0000000000..879d3e6ad6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/DataMigrationOperation.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * A [Runnable] used to perform a data migration operation (moving, modifying or deleting files). + */ +@NoOpImplementation +internal interface DataMigrationOperation : Runnable diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/DataMigrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/DataMigrator.kt new file mode 100644 index 0000000000..28d991b7cf --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/DataMigrator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import androidx.annotation.WorkerThread +import com.datadog.android.core.internal.persistence.file.FileOrchestrator + +internal interface DataMigrator { + + @WorkerThread + fun migrateData( + previousState: S?, + previousFileOrchestrator: FileOrchestrator, + newState: S, + newFileOrchestrator: FileOrchestrator + ) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestrator.kt new file mode 100644 index 0000000000..c776b234c6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestrator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.metrics.MetricsDispatcher +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.batch.BatchFileOrchestrator +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.privacy.TrackingConsent +import java.io.File +import java.util.Locale +import java.util.concurrent.ExecutorService + +internal class FeatureFileOrchestrator( + consentProvider: ConsentProvider, + pendingOrchestrator: FileOrchestrator, + grantedOrchestrator: FileOrchestrator, + dataMigrator: DataMigrator, + executorService: ExecutorService, + internalLogger: InternalLogger +) : ConsentAwareFileOrchestrator( + consentProvider, + pendingOrchestrator, + grantedOrchestrator, + dataMigrator, + executorService, + internalLogger +) { + + constructor( + consentProvider: ConsentProvider, + storageDir: File, + featureName: String, + executorService: ExecutorService, + filePersistenceConfig: FilePersistenceConfig, + internalLogger: InternalLogger, + metricsDispatcher: MetricsDispatcher + ) : this( + consentProvider, + BatchFileOrchestrator( + File(storageDir, PENDING_DIR.format(Locale.US, featureName)), + filePersistenceConfig, + internalLogger, + metricsDispatcher + ), + BatchFileOrchestrator( + File(storageDir, GRANTED_DIR.format(Locale.US, featureName)), + filePersistenceConfig, + internalLogger, + metricsDispatcher + ), + ConsentAwareFileMigrator( + FileMover(internalLogger), + internalLogger + ), + executorService, + internalLogger + ) + + companion object { + private const val BASE_DIR_NAME_REG_EX = "([a-z]+-)+" + internal val IS_GRANTED_DIR_REG_EX = Regex("${BASE_DIR_NAME_REG_EX}v[0-9]+") + internal val IS_PENDING_DIR_REG_EX = Regex("${BASE_DIR_NAME_REG_EX}pending-v[0-9]+") + + internal const val VERSION = 2 + internal const val PENDING_DIR = "%s-pending-v$VERSION" + internal const val GRANTED_DIR = "%s-v$VERSION" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation.kt new file mode 100644 index 0000000000..e043c35586 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.utils.retryWithDelay +import java.io.File +import java.util.concurrent.TimeUnit + +/** + * A [DataMigrationOperation] that moves all the files in the `fromDir` directory + * to the `toDir` directory. + */ +internal class MoveDataMigrationOperation( + internal val fromDir: File?, + internal val toDir: File?, + internal val fileMover: FileMover, + internal val internalLogger: InternalLogger +) : DataMigrationOperation { + + @WorkerThread + override fun run() { + if (fromDir == null) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { WARN_NULL_SOURCE_DIR } + ) + } else if (toDir == null) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { WARN_NULL_DEST_DIR } + ) + } else { + retryWithDelay(MAX_RETRY, RETRY_DELAY_NS, internalLogger) { + fileMover.moveFiles(fromDir, toDir) + } + } + } + + companion object { + internal const val WARN_NULL_SOURCE_DIR = "Can't move data from a null directory" + internal const val WARN_NULL_DEST_DIR = "Can't move data to a null directory" + + private const val MAX_RETRY = 3 + private val RETRY_DELAY_NS = TimeUnit.MILLISECONDS.toNanos(500) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ScheduledWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ScheduledWriter.kt new file mode 100644 index 0000000000..0253634add --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ScheduledWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.DataWriter +import com.datadog.android.core.internal.utils.executeSafe +import java.util.concurrent.ExecutorService + +internal class ScheduledWriter( + internal val delegateWriter: DataWriter, + internal val executorService: ExecutorService, + private val internalLogger: InternalLogger +) : DataWriter { + + // region DataWriter + + @WorkerThread + override fun write(element: T) { + executorService.executeSafe("Data writing", internalLogger) { + delegateWriter.write(element) + } + } + + @WorkerThread + override fun write(data: List) { + executorService.executeSafe("Data writing", internalLogger) { + delegateWriter.write(data) + } + } + + // endregion +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation.kt new file mode 100644 index 0000000000..cf120e0e14 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.utils.retryWithDelay +import java.io.File +import java.util.concurrent.TimeUnit + +/** + * A [DataMigrationOperation] that delete all the files in the `targetDir` directory. + */ +internal class WipeDataMigrationOperation( + internal val targetDir: File?, + internal val fileMover: FileMover, + internal val internalLogger: InternalLogger +) : DataMigrationOperation { + + @WorkerThread + override fun run() { + if (targetDir == null) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { WARN_NULL_DIR } + ) + } else { + retryWithDelay(MAX_RETRY, RETRY_DELAY_NS, internalLogger) { + fileMover.delete(targetDir) + } + } + } + + companion object { + internal const val WARN_NULL_DIR = "Can't wipe data from a null directory" + + private const val MAX_RETRY = 3 + private val RETRY_DELAY_NS = TimeUnit.MILLISECONDS.toNanos(500) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileDataReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileDataReader.kt new file mode 100644 index 0000000000..3d310fd97d --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileDataReader.kt @@ -0,0 +1,148 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.Batch +import com.datadog.android.core.internal.persistence.DataReader +import com.datadog.android.core.internal.persistence.PayloadDecoration +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.utils.join +import java.io.File +import java.util.Locale + +/** + * A [DataReader] reading [Batch] data from files. + */ +internal class BatchFileDataReader( + internal val fileOrchestrator: FileOrchestrator, + internal val decoration: PayloadDecoration, + internal val fileReader: BatchFileReader, + internal val fileMover: FileMover, + internal val internalLogger: InternalLogger +) : DataReader { + + private val lockedFiles: MutableList = mutableListOf() + + // region DataReader + + @WorkerThread + override fun lockAndReadNext(): Batch? { + val file = getAndLockReadableFile() ?: return null + val data = fileReader.readData(file) + .map { it.data } + .join( + separator = decoration.separatorBytes, + prefix = decoration.prefixBytes, + suffix = decoration.suffixBytes, + internalLogger + ) + + return Batch(file.name, data) + } + + @WorkerThread + override fun release(data: Batch) { + releaseFile(data.id, delete = false) + } + + @WorkerThread + override fun drop(data: Batch) { + releaseFile(data.id, delete = true) + } + + @WorkerThread + override fun dropAll() { + synchronized(lockedFiles) { + lockedFiles.toTypedArray().forEach { + releaseFile(it, delete = true) + } + } + + fileOrchestrator.getAllFiles().forEach { + val metaFile = fileOrchestrator.getMetadataFile(it) + deleteFile(it) + if (metaFile?.existsSafe(internalLogger) == true) { + deleteFile(metaFile) + } + } + } + + // endregion + + // region Internal + + @WorkerThread + private fun getAndLockReadableFile(): File? { + synchronized(lockedFiles) { + val readableFile = fileOrchestrator.getReadableFile(lockedFiles.toSet()) + if (readableFile != null) { + lockedFiles.add(readableFile) + } + return readableFile + } + } + + @WorkerThread + private fun releaseFile( + fileName: String, + delete: Boolean + ) { + val file = synchronized(lockedFiles) { + lockedFiles.firstOrNull { it.name == fileName } + } + if (file != null) { + releaseFile(file, delete) + } else { + internalLogger.log( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + { WARNING_UNKNOWN_BATCH_ID.format(Locale.US, fileName) } + ) + } + } + + @WorkerThread + private fun releaseFile( + file: File, + delete: Boolean + ) { + if (delete) { + val metaFile = fileOrchestrator.getMetadataFile(file) + deleteFile(file) + if (metaFile?.existsSafe(internalLogger) == true) { + deleteFile(metaFile) + } + } + synchronized(lockedFiles) { + lockedFiles.remove(file) + } + } + + @WorkerThread + private fun deleteFile(file: File) { + if (!fileMover.delete(file)) { + internalLogger.log( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + { WARNING_DELETE_FAILED.format(Locale.US, file.path) } + ) + } + } + + // endregion + + internal companion object { + internal const val WARNING_UNKNOWN_BATCH_ID = + "Attempting to unlock or delete an unknown file: %s" + internal const val WARNING_DELETE_FAILED = + "Unable to delete file: %s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator.kt new file mode 100644 index 0000000000..dc689cdd38 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator.kt @@ -0,0 +1,372 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.metrics.BatchClosedMetadata +import com.datadog.android.core.internal.metrics.MetricsDispatcher +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.canWriteSafe +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.lengthSafe +import com.datadog.android.core.internal.persistence.file.listFilesSafe +import com.datadog.android.core.internal.persistence.file.mkdirsSafe +import java.io.File +import java.io.FileFilter +import java.util.Locale +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.roundToLong + +// TODO RUM-438 Improve this class: need to make it thread-safe and optimize work with file +// system in order to reduce the number of syscalls (which are expensive) for files already seen +@Suppress("TooManyFunctions") +internal class BatchFileOrchestrator( + private val rootDir: File, + internal val config: FilePersistenceConfig, + private val internalLogger: InternalLogger, + private val metricsDispatcher: MetricsDispatcher, + private val pendingFiles: AtomicInteger = AtomicInteger(0) +) : FileOrchestrator { + + private val fileFilter = BatchFileFilter() + + // Offset the recent threshold for read and write to avoid conflicts + // Arbitrary offset as ±5% of the threshold + @Suppress("UnsafeThirdPartyFunctionCall") // rounded Double isn't NaN + private val recentReadDelayMs = (config.recentDelayMs * INCREASE_PERCENT).roundToLong() + + @Suppress("UnsafeThirdPartyFunctionCall") // rounded Double isn't NaN + private val recentWriteDelayMs = (config.recentDelayMs * DECREASE_PERCENT).roundToLong() + + // keep track of how many items were written in the last known file + private var previousFile: File? = null + private var previousFileItemCount: Long = 0 + private var lastFileAccessTimestamp: Long = 0L + private var lastCleanupTimestamp: Long = 0L + + // region FileOrchestrator + + @WorkerThread + override fun getWritableFile(): File? { + if (!isRootDirValid()) { + return null + } + + if (canDoCleanup()) { + var files = listBatchFiles() + files = deleteObsoleteFiles(files) + freeSpaceIfNeeded(files) + lastCleanupTimestamp = System.currentTimeMillis() + } + + return getReusableWritableFile() ?: createNewFile() + } + + @WorkerThread + override fun getReadableFile(excludeFiles: Set): File? { + if (!isRootDirValid()) { + return null + } + + val files = listSortedBatchFiles().let { + deleteObsoleteFiles(it) + } + lastCleanupTimestamp = System.currentTimeMillis() + pendingFiles.set(files.count()) + + return files.firstOrNull { + (it !in excludeFiles) && !isFileRecent(it, recentReadDelayMs) + } + } + + @WorkerThread + override fun getAllFiles(): List { + if (!isRootDirValid()) { + return emptyList() + } + + return listSortedBatchFiles() + } + + @WorkerThread + override fun getFlushableFiles(): List { + return getAllFiles() + } + + @WorkerThread + override fun getRootDir(): File? { + if (!isRootDirValid()) { + return null + } + + return rootDir + } + + override fun getRootDirName(): String { + return rootDir.nameWithoutExtension + } + + @WorkerThread + override fun getMetadataFile(file: File): File? { + if (file.parent != rootDir.path) { + // may happen if batch file was requested with pending orchestrator, but meta file + // is requested with granted orchestrator (due to consent change). Not an issue, because + // batch file should be migrated to the same folder, but leaving this debug point + // just in case. + internalLogger.log( + InternalLogger.Level.DEBUG, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { DEBUG_DIFFERENT_ROOT.format(Locale.US, file.path, rootDir.path) } + ) + } + + return if (file.isBatchFile) { + file.metadata + } else { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_NOT_BATCH_FILE.format(Locale.US, file.path) } + ) + null + } + } + + // endregion + + // region Internal + + override fun decrementAndGetPendingFilesCount(): Int { + return pendingFiles.decrementAndGet() + } + + @Suppress("LiftReturnOrAssignment", "ReturnCount") + private fun isRootDirValid(): Boolean { + if (rootDir.existsSafe(internalLogger)) { + if (rootDir.isDirectory) { + if (rootDir.canWriteSafe(internalLogger)) { + return true + } else { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { ERROR_ROOT_NOT_WRITABLE.format(Locale.US, rootDir.path) } + ) + return false + } + } else { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { ERROR_ROOT_NOT_DIR.format(Locale.US, rootDir.path) } + ) + return false + } + } else { + synchronized(rootDir) { + // double check if directory was already created by some other thread + // entered this branch + if (rootDir.existsSafe(internalLogger)) { + return true + } + + if (rootDir.mkdirsSafe(internalLogger)) { + return true + } else { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { ERROR_CANT_CREATE_ROOT.format(Locale.US, rootDir.path) } + ) + return false + } + } + } + } + + private fun createNewFile(): File { + val newFileName = System.currentTimeMillis().toString() + val newFile = File(rootDir, newFileName) + val closedFile = previousFile + val closedFileLastAccessTimestamp = lastFileAccessTimestamp + if (closedFile != null) { + metricsDispatcher.sendBatchClosedMetric( + closedFile, + BatchClosedMetadata( + lastTimeWasUsedInMs = closedFileLastAccessTimestamp, + eventsCount = previousFileItemCount + ) + ) + } + previousFile = newFile + previousFileItemCount = 1 + lastFileAccessTimestamp = System.currentTimeMillis() + pendingFiles.incrementAndGet() + return newFile + } + + @Suppress("ReturnCount") + private fun getReusableWritableFile(): File? { + val files = listBatchFiles() + val lastFile = files.latestBatchFile ?: return null + + val lastKnownFile = previousFile + val lastKnownFileItemCount = previousFileItemCount + if (lastKnownFile != lastFile) { + // this situation can happen because: + // 1. `lastFile` is a file written during a previous session + // 2. `lastFile` was created by another system/process + // 3. `lastKnownFile` was deleted + // In any case, we don't know the item count, so to be safe, we create a new file + return null + } + + val isRecentEnough = isFileRecent(lastFile, recentWriteDelayMs) + val hasRoomForMore = lastFile.lengthSafe(internalLogger) < config.maxBatchSize + val hasSlotForMore = (lastKnownFileItemCount < config.maxItemsPerBatch) + + return if (isRecentEnough && hasRoomForMore && hasSlotForMore) { + previousFileItemCount = lastKnownFileItemCount + 1 + lastFileAccessTimestamp = System.currentTimeMillis() + lastFile + } else { + null + } + } + + private fun isFileRecent(file: File, delayMs: Long): Boolean { + val now = System.currentTimeMillis() + val fileTimestamp = file.name.toLongOrNull() ?: 0L + return fileTimestamp >= (now - delayMs) + } + + private fun deleteObsoleteFiles(files: List): List { + val threshold = System.currentTimeMillis() - config.oldFileThreshold + return files + .mapNotNull { + val isOldFile = (it.name.toLongOrNull() ?: 0) < threshold + if (isOldFile) { + if (it.deleteSafe(internalLogger)) { + metricsDispatcher.sendBatchDeletedMetric( + batchFile = it, + removalReason = RemovalReason.Obsolete, + numPendingBatches = pendingFiles.decrementAndGet() + ) + } + if (it.metadata.existsSafe(internalLogger)) { + it.metadata.deleteSafe(internalLogger) + } + null + } else { + it + } + } + } + + private fun freeSpaceIfNeeded(files: List) { + val sizeOnDisk = files.sumOf { it.lengthSafe(internalLogger) } + val maxDiskSpace = config.maxDiskSpace + val sizeToFree = sizeOnDisk - maxDiskSpace + if (sizeToFree > 0) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_DISK_FULL.format(Locale.US, sizeOnDisk, maxDiskSpace, sizeToFree) } + ) + files.sorted().fold(sizeToFree) { remainingSizeToFree, file -> + if (remainingSizeToFree > 0) { + val deletedFileSize = deleteFile(file, true) + val deletedMetaFileSize = deleteFile(file.metadata) + remainingSizeToFree - deletedFileSize - deletedMetaFileSize + } else { + remainingSizeToFree + } + } + } + } + + private fun deleteFile(file: File, sendMetric: Boolean = false): Long { + if (!file.existsSafe(internalLogger)) return 0 + + val size = file.lengthSafe(internalLogger) + val wasDeleted = file.deleteSafe(internalLogger) + return if (wasDeleted) { + if (sendMetric) { + metricsDispatcher.sendBatchDeletedMetric(file, RemovalReason.Purged, pendingFiles.decrementAndGet()) + } + size + } else { + 0 + } + } + + private fun listBatchFiles(): List { + return rootDir.listFilesSafe(fileFilter, internalLogger).orEmpty().toList() + } + + private fun listSortedBatchFiles(): List { + // note: since it is using File#compareTo, lexicographical sorting will be used, meaning "10" comes before "9". + // but for our needs it is fine, because the moment when Unix timestamp adds one more digit will be in 2286. + return listBatchFiles().sorted() + } + + private fun canDoCleanup(): Boolean { + return System.currentTimeMillis() - lastCleanupTimestamp > config.cleanupFrequencyThreshold + } + + private val File.metadata: File + get() = File("${this.path}_metadata") + + private val File.isBatchFile: Boolean + get() = name.toLongOrNull() != null + + private val List.latestBatchFile: File? + get() = maxOrNull() + + // endregion + + // region FileFilter + + internal inner class BatchFileFilter : FileFilter { + @Suppress("ReturnCount") + override fun accept(file: File?): Boolean { + if (file == null) return false + + return file.isBatchFile + } + } + + // endregion + + companion object { + + const val DECREASE_PERCENT = 0.95 + const val INCREASE_PERCENT = 1.05 + + internal const val ERROR_ROOT_NOT_WRITABLE = "The provided root dir is not writable: %s" + internal const val ERROR_ROOT_NOT_DIR = "The provided root file is not a directory: %s" + internal const val ERROR_CANT_CREATE_ROOT = "The provided root dir can't be created: %s" + internal const val ERROR_DISK_FULL = "Too much disk space used (%d/%d): " + + "cleaning up to free %d bytes…" + internal const val ERROR_NOT_BATCH_FILE = "The file provided is not a batch file: %s" + internal const val DEBUG_DIFFERENT_ROOT = "The file provided (%s) doesn't belong" + + " to the current folder (%s)" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReader.kt new file mode 100644 index 0000000000..1fc8ea2c0a --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import androidx.annotation.WorkerThread +import com.datadog.android.api.storage.RawBatchEvent +import java.io.File + +internal interface BatchFileReader { + + /** + * Reads data from the given file. + * @param file the file to read from + * @return the list of events as [RawBatchEvent] data stored in a file. + */ + @WorkerThread + fun readData( + file: File + ): List +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriter.kt new file mode 100644 index 0000000000..85533e1b17 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.file.FileWriter +import com.datadog.android.security.Encryption + +internal interface BatchFileReaderWriter : FileWriter, BatchFileReader { + + companion object { + /** + * Creates either plain [PlainBatchFileReaderWriter] or [PlainBatchFileReaderWriter] wrapped in + * [EncryptedBatchReaderWriter] if encryption is provided. + */ + fun create(internalLogger: InternalLogger, encryption: Encryption?): BatchFileReaderWriter { + val readerWriter = PlainBatchFileReaderWriter(internalLogger) + return if (encryption == null) { + readerWriter + } else { + EncryptedBatchReaderWriter( + encryption, + readerWriter, + internalLogger + ) + } + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/EncryptedBatchReaderWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/EncryptedBatchReaderWriter.kt new file mode 100644 index 0000000000..c18f2417f1 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/EncryptedBatchReaderWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.security.Encryption +import java.io.File + +internal class EncryptedBatchReaderWriter( + internal val encryption: Encryption, + internal val delegate: BatchFileReaderWriter, + private val internalLogger: InternalLogger +) : BatchFileReaderWriter by delegate { + + @WorkerThread + override fun writeData( + file: File, + data: RawBatchEvent, + append: Boolean + ): Boolean { + val encryptedRawBatchEvent = RawBatchEvent( + data = encryption.encrypt(data.data), + metadata = encryption.encrypt(data.metadata) + ) + + if (data.data.isNotEmpty() && encryptedRawBatchEvent.data.isEmpty()) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { BAD_ENCRYPTION_RESULT_MESSAGE } + ) + return false + } + + return delegate.writeData( + file, + encryptedRawBatchEvent, + append + ) + } + + @WorkerThread + override fun readData( + file: File + ): List { + return delegate.readData(file) + .map { + RawBatchEvent( + data = if (it.data.isNotEmpty()) encryption.decrypt(it.data) else it.data, + metadata = if (it.metadata.isNotEmpty()) encryption.decrypt(it.metadata) else it.metadata + ) + } + } + + companion object { + internal const val BAD_ENCRYPTION_RESULT_MESSAGE = "Encryption of non-empty data produced" + + " empty result, aborting write operation." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/PlainBatchFileReaderWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/PlainBatchFileReaderWriter.kt new file mode 100644 index 0000000000..7b90f24d1a --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/PlainBatchFileReaderWriter.kt @@ -0,0 +1,273 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.file.lengthSafe +import com.datadog.android.core.internal.utils.use +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.Locale +import kotlin.math.max + +/** + * Stores data in the TLV format as meta+data, use only for RUM/Log/Trace events. + */ +internal class PlainBatchFileReaderWriter( + private val internalLogger: InternalLogger +) : BatchFileReaderWriter { + + // region FileWriter + + @WorkerThread + override fun writeData( + file: File, + data: RawBatchEvent, + append: Boolean + ): Boolean { + return try { + lockFileAndWriteData(file, append, data) + true + } catch (e: IOException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER), + { ERROR_WRITE.format(Locale.US, file.path) }, + e + ) + false + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_WRITE.format(Locale.US, file.path) }, + e + ) + false + } + } + + // endregion + + // region FileReader + + @WorkerThread + override fun readData( + file: File + ): List { + return try { + readFileData(file) + } catch (e: IOException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_READ.format(Locale.US, file.path) }, + e + ) + emptyList() + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_READ.format(Locale.US, file.path) }, + e + ) + emptyList() + } + } + + // endregion + + // region Internal + + @Throws(IOException::class) + @Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block + private fun lockFileAndWriteData( + file: File, + append: Boolean, + data: RawBatchEvent + ) { + FileOutputStream(file, append).use { outputStream -> + outputStream.channel.lock().use { + val meta = data.metadata + + val metaBlockSize = TYPE_SIZE_BYTES + LENGTH_SIZE_BYTES + meta.size + val dataBlockSize = TYPE_SIZE_BYTES + LENGTH_SIZE_BYTES + data.data.size + + // ByteBuffer by default has BigEndian ordering, which matches to how Java + // reads data, so no need to define it explicitly + val buffer = ByteBuffer + .allocate(metaBlockSize + dataBlockSize) + .putAsTlv(BlockType.META, meta) + .putAsTlv(BlockType.EVENT, data.data) + + outputStream.write(buffer.array()) + } + } + } + + @Throws(IOException::class) + @Suppress("UnsafeThirdPartyFunctionCall", "ComplexMethod", "LoopWithTooManyJumpStatements") + // Called within a try/catch block + private fun readFileData( + file: File + ): List { + val inputLength = file.lengthSafe(internalLogger).toInt() + + val result = mutableListOf() + + // Read file iteratively + var remaining = inputLength + file.inputStream().buffered().use { + while (remaining > 0) { + val metaReadResult = readBlock(it, BlockType.META) + if (metaReadResult.data == null) { + remaining -= metaReadResult.bytesRead + break + } + + val eventReadResult = readBlock(it, BlockType.EVENT) + remaining -= metaReadResult.bytesRead + eventReadResult.bytesRead + + if (eventReadResult.data == null) break + + result.add(RawBatchEvent(eventReadResult.data, metaReadResult.data)) + } + } + + if (remaining != 0 || (inputLength > 0 && result.isEmpty())) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { WARNING_NOT_ALL_DATA_READ.format(Locale.US, file.path) } + ) + } + + return result + } + + @Suppress("ReturnCount") + @Throws(IOException::class) + private fun readBlock(stream: InputStream, expectedBlockType: BlockType): BlockReadResult { + @Suppress("UnsafeThirdPartyFunctionCall") // allocation size is always positive + val headerBuffer = ByteBuffer.allocate(HEADER_SIZE_BYTES) + + @Suppress("UnsafeThirdPartyFunctionCall") // method declares throwing IOException + val headerReadBytes = stream.read(headerBuffer.array()) + + if (!checkReadExpected( + HEADER_SIZE_BYTES, + headerReadBytes, + "Block(${expectedBlockType.name}): Header read" + ) + ) { + return BlockReadResult(null, max(0, headerReadBytes)) + } + + val blockType = headerBuffer.short + if (blockType != expectedBlockType.identifier) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { + "Unexpected block type identifier=$blockType met," + + " was expecting $expectedBlockType(${expectedBlockType.identifier})" + } + ) + // in theory we could continue reading, because we still know data size, + // but unexpected type says that at least relationship between blocks is broken, + // so to not establish the wrong one, it is better to stop reading + return BlockReadResult(null, headerReadBytes) + } + + val dataSize = headerBuffer.int + val dataBuffer = ByteArray(dataSize) + + @Suppress("UnsafeThirdPartyFunctionCall") // method declares throwing IOException + val dataReadBytes = stream.read(dataBuffer) + + return if (checkReadExpected( + dataSize, + dataReadBytes, + "Block(${expectedBlockType.name}):Data read" + ) + ) { + BlockReadResult(dataBuffer, headerReadBytes + dataReadBytes) + } else { + BlockReadResult(null, headerReadBytes + max(0, dataReadBytes)) + } + } + + private fun checkReadExpected(expected: Int, actual: Int, operation: String): Boolean { + return if (expected != actual) { + if (actual != -1) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { + "Number of bytes read for operation='$operation' doesn't" + + " match with expected: expected=$expected, actual=$actual" + } + ) + } else { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Unexpected EOF at the operation=$operation" } + ) + } + false + } else { + true + } + } + + @Suppress("UnsafeThirdPartyFunctionCall") + // all calls here are safe: buffer is writable and it has a proper size calculated before + // Encoding specification is as following: + // +- 2 bytes -+- 4 bytes -+- n bytes -| + // | block type | data size (n) | data | + // +------------+---------------+-----------+ + // where block type is 0x00 for event, 0x01 for data + private fun ByteBuffer.putAsTlv(blockType: BlockType, data: ByteArray): ByteBuffer { + return this + .putShort(blockType.identifier) + .putInt(data.size) + .put(data) + } + + private class BlockReadResult( + val data: ByteArray?, + val bytesRead: Int + ) + + private enum class BlockType(val identifier: Short) { + EVENT(0x00), + META(0x01) + } + + // endregion + + companion object { + + // TLV (Type-Length-Value) constants + internal const val TYPE_SIZE_BYTES: Int = 2 + internal const val LENGTH_SIZE_BYTES: Int = 4 + internal const val HEADER_SIZE_BYTES: Int = TYPE_SIZE_BYTES + LENGTH_SIZE_BYTES + + internal const val ERROR_WRITE = "Unable to write data to file: %s" + internal const val ERROR_READ = "Unable to read data from file: %s" + + internal const val WARNING_NOT_ALL_DATA_READ = + "File %s is probably corrupted, not all content was read." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleFileOrchestrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleFileOrchestrator.kt new file mode 100644 index 0000000000..75b2a768e6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleFileOrchestrator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.single + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.mkdirsSafe +import java.io.File + +internal class SingleFileOrchestrator( + private val file: File, + private val internalLogger: InternalLogger +) : FileOrchestrator { + + // region FileOrchestrator + + @WorkerThread + override fun getWritableFile(): File? { + file.parentFile?.mkdirsSafe(internalLogger) + return file + } + + @WorkerThread + override fun getReadableFile(excludeFiles: Set): File? { + file.parentFile?.mkdirsSafe(internalLogger) + return if (file in excludeFiles) { + null + } else { + file + } + } + + @WorkerThread + override fun getAllFiles(): List { + file.parentFile?.mkdirsSafe(internalLogger) + return listOf(file) + } + + @WorkerThread + override fun getRootDir(): File? { + return null + } + + @WorkerThread + override fun getFlushableFiles(): List { + return getAllFiles() + } + + @WorkerThread + override fun getMetadataFile(file: File): File? { + return null + } + + override fun getRootDirName(): String? { + return file.parentFile?.nameWithoutExtension + } + + // single file orchestrator has a single file, so this is essentially a noop implementation + override fun decrementAndGetPendingFilesCount(): Int { + return 0 + } + + // endregion +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleItemDataWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleItemDataWriter.kt new file mode 100644 index 0000000000..7b131649c0 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleItemDataWriter.kt @@ -0,0 +1,86 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.single + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.DataWriter +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileWriter +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.core.persistence.serializeToByteArray +import java.util.Locale + +internal open class SingleItemDataWriter( + internal val fileOrchestrator: FileOrchestrator, + internal val serializer: Serializer, + internal val fileWriter: FileWriter, + internal val internalLogger: InternalLogger, + internal val filePersistenceConfig: FilePersistenceConfig +) : DataWriter { + + // region DataWriter + + @WorkerThread + override fun write(element: T) { + consume(element) + } + + @WorkerThread + override fun write(data: List) { + val element = data.lastOrNull() ?: return + consume(element) + } + + // endregion + + // region Internal + + @WorkerThread + private fun consume(data: T) { + val byteArray = serializer.serializeToByteArray(data, internalLogger) ?: return + + synchronized(this) { + writeData(byteArray) + } + } + + @Suppress("ReturnCount") + @WorkerThread + private fun writeData(byteArray: ByteArray): Boolean { + if (!checkEventSize(byteArray.size)) return false + val file = fileOrchestrator.getWritableFile() ?: return false + return fileWriter.writeData(file, byteArray, false) + } + + private fun checkEventSize(eventSize: Int): Boolean { + if (eventSize > filePersistenceConfig.maxItemSize) { + // DISCUSS? send a RUM/Log Error event here to the org so they get visibility + // about this in their own org? + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { + ERROR_LARGE_DATA.format( + Locale.US, + eventSize, + filePersistenceConfig.maxItemSize + ) + } + ) + return false + } + return true + } + + // endregion + + companion object { + internal const val ERROR_LARGE_DATA = "Can't write data with size %d (max item size is %d)" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt new file mode 100644 index 0000000000..d79bf7cb1c --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import com.datadog.android.api.InternalLogger +import java.nio.ByteBuffer +import java.util.Locale + +internal class TLVBlock( + val type: TLVBlockType, + val data: ByteArray, + val internalLogger: InternalLogger +) { + @Suppress("ReturnCount") + internal fun serialize(maxEntrySize: Int = MAXIMUM_DATA_SIZE_MB): ByteArray? { + if (data.isEmpty()) return null + + val typeFieldSize = Short.SIZE_BYTES + val dataLengthFieldSize = Int.SIZE_BYTES + val dataFieldSize = data.size + + val entrySize = typeFieldSize + dataLengthFieldSize + dataFieldSize + + if (entrySize > maxEntrySize) { + logEntrySizeExceededError(entrySize, maxEntrySize) + return null + } + + val tlvTypeAsShort = type.rawValue.toShort() + + // capacity is not a negative integer, buffer is not read only, + // has sufficient capacity and is backed by an array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer + .allocate(entrySize) + .putShort(tlvTypeAsShort) + .putInt(dataFieldSize) + .put(data) + .array() + } + + private fun logEntrySizeExceededError(entrySize: Int, maxEntrySize: Int) { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { BYTE_LENGTH_EXCEEDED_ERROR.format(Locale.US, maxEntrySize, entrySize) } + ) + } + + internal companion object { + // The maximum length of data (Value) in TLV block defining key data. + private const val MAXIMUM_DATA_SIZE_MB = 10 * 1024 * 1024 // 10 mb + internal const val BYTE_LENGTH_EXCEEDED_ERROR = + "DataBlock length exceeds limit of %s bytes, was %s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt new file mode 100644 index 0000000000..a5f8db3683 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.utils.copyOfRangeSafe +import com.datadog.android.core.internal.utils.toInt +import com.datadog.android.core.internal.utils.toShort +import java.io.File +import java.util.Locale + +internal class TLVBlockFileReader( + val internalLogger: InternalLogger, + val fileReaderWriter: FileReaderWriter +) { + @WorkerThread + internal fun read( + file: File + ): List { + val byteArray = fileReaderWriter.readData(file) + val blocks = mutableListOf() + var currentIndex = 0 + + while (currentIndex < byteArray.size) { + val result = readBlock(byteArray, currentIndex) ?: break + blocks.add(result.data) + currentIndex = result.newIndex + } + + return blocks + } + + @Suppress("ReturnCount") + private fun readBlock(inputArray: ByteArray, currentIndex: Int): TLVResult? { + val typeResult = readType(inputArray, currentIndex) ?: return null + val data = readData(inputArray, typeResult.newIndex) ?: return null + + val block = TLVBlock(typeResult.data, data.data, internalLogger) + return TLVResult( + data = block, + newIndex = data.newIndex + ) + } + + @Suppress("ReturnCount") + private fun readType(inputArray: ByteArray, currentIndex: Int): TLVResult? { + val typeBlockSize = UShort.SIZE_BYTES + var newIndex = currentIndex + newIndex += typeBlockSize + + if (newIndex > inputArray.size) { + logFailedToDeserializeError() + return null + } + + val bytes = inputArray.copyOfRangeSafe(currentIndex, newIndex) + + val shortValue = bytes.toShort() + + val tlvHeader = TLVBlockType.fromValue(shortValue.toUShort()) + + if (tlvHeader == null) { + logTypeCorruptionError(shortValue) + return null + } + + return TLVResult( + data = tlvHeader, + newIndex = currentIndex + typeBlockSize + ) + } + + private fun readData(inputArray: ByteArray, currentIndex: Int): TLVResult? { + val lengthBlockSize = Int.SIZE_BYTES + var newIndex = currentIndex + lengthBlockSize + + if (newIndex > inputArray.size) { + logFailedToDeserializeError() + return null + } + + val lengthInBytes = inputArray.copyOfRangeSafe(currentIndex, newIndex) + + val lengthData = lengthInBytes.toInt() + + val dataBytes = + inputArray.copyOfRangeSafe(newIndex, newIndex + lengthData) + + newIndex += lengthData + + return TLVResult( + data = dataBytes, + newIndex = newIndex + ) + } + + private fun logTypeCorruptionError(shortValue: Short) { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { + CORRUPT_TLV_HEADER_TYPE_ERROR.format( + Locale.US, + shortValue + ) + } + ) + } + + private fun logFailedToDeserializeError() { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { FAILED_TO_DESERIALIZE_ERROR } + ) + } + + private data class TLVResult( + val data: T, + val newIndex: Int + ) + + internal companion object { + internal const val CORRUPT_TLV_HEADER_TYPE_ERROR = "TLV header corrupt. Invalid type %s" + internal const val FAILED_TO_DESERIALIZE_ERROR = "Failed to deserialize TLV data length" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt new file mode 100644 index 0000000000..163df94f1a --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +internal enum class TLVBlockType(val rawValue: UShort) { + VERSION_CODE(0x00u), + DATA(0x01u); + + companion object { + private val map = values().associateBy { it.rawValue } + + fun fromValue(value: UShort): TLVBlockType? { + return map[value] + } + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/privacy/ConsentProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/privacy/ConsentProvider.kt similarity index 91% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/privacy/ConsentProvider.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/privacy/ConsentProvider.kt index e9124290b6..dcd2683f07 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/privacy/ConsentProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/privacy/ConsentProvider.kt @@ -19,5 +19,7 @@ internal interface ConsentProvider { fun registerCallback(callback: TrackingConsentProviderCallback) + fun unregisterCallback(callback: TrackingConsentProviderCallback) + fun unregisterAllCallbacks() } diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/privacy/TrackingConsentProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/privacy/TrackingConsentProvider.kt similarity index 88% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/privacy/TrackingConsentProvider.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/privacy/TrackingConsentProvider.kt index 18ec94f54f..6c4306571c 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/privacy/TrackingConsentProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/privacy/TrackingConsentProvider.kt @@ -10,7 +10,7 @@ import com.datadog.android.privacy.TrackingConsent import com.datadog.android.privacy.TrackingConsentProviderCallback import java.util.LinkedList -internal class TrackingConsentProvider(consent: TrackingConsent = TrackingConsent.PENDING) : +internal class TrackingConsentProvider(consent: TrackingConsent) : ConsentProvider { private val callbacks: LinkedList = LinkedList() @@ -45,6 +45,11 @@ internal class TrackingConsentProvider(consent: TrackingConsent = TrackingConsen callbacks.add(callback) } + @Synchronized + override fun unregisterCallback(callback: TrackingConsentProviderCallback) { + callbacks.remove(callback) + } + @Synchronized override fun unregisterAllCallbacks() { callbacks.clear() diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiver.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiver.kt new file mode 100644 index 0000000000..a652f6d931 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiver.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.receiver + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import java.util.concurrent.atomic.AtomicBoolean + +internal abstract class ThreadSafeReceiver : BroadcastReceiver() { + + val isRegistered = AtomicBoolean(false) + + // We suppress the warning here as this method is not available on all Android versions + @SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag") + fun registerReceiver( + context: Context, + filter: IntentFilter + ): Intent? { + val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.registerReceiver(this, filter, RECEIVER_NOT_EXPORTED_COMPAT) + } else { + context.registerReceiver(this, filter) + } + isRegistered.set(true) + return intent + } + + fun unregisterReceiver(context: Context) { + if (isRegistered.compareAndSet(true, false)) { + context.unregisterReceiver(this) + } + } + + companion object { + internal const val RECEIVER_NOT_EXPORTED_COMPAT: Int = 0x4 + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/AndroidInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/AndroidInfoProvider.kt new file mode 100644 index 0000000000..c4c0569599 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/AndroidInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import com.datadog.android.api.context.DeviceType + +internal interface AndroidInfoProvider { + + val deviceName: String + + val deviceBrand: String + + val deviceModel: String + + val deviceType: DeviceType + + val deviceBuildId: String + + val osName: String + + val osMajorVersion: String + + val osVersion: String + + val architecture: String + + val numberOfDisplays: Int? + + val locales: List + + val currentLocale: String + + val timeZone: String +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/AppVersionProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/AppVersionProvider.kt new file mode 100644 index 0000000000..d671f4af04 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/AppVersionProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +internal interface AppVersionProvider { + var version: String +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProvider.kt new file mode 100644 index 0000000000..8b018dc535 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.PowerManager +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.receiver.ThreadSafeReceiver +import kotlin.math.roundToInt + +internal class BroadcastReceiverSystemInfoProvider( + private val internalLogger: InternalLogger +) : + ThreadSafeReceiver(), SystemInfoProvider { + + private var systemInfo: SystemInfo = SystemInfo() + + // region BroadcastReceiver + + override fun onReceive(context: Context, intent: Intent?) { + try { + when (val action = intent?.action) { + Intent.ACTION_BATTERY_CHANGED -> { + handleBatteryIntent(intent) + } + + PowerManager.ACTION_POWER_SAVE_MODE_CHANGED -> { + handlePowerSaveIntent(context) + } + + else -> { + internalLogger.log( + InternalLogger.Level.DEBUG, + listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { "Received unknown broadcast intent: [$action]" } + ) + } + } + } catch (@Suppress("TooGenericExceptionCaught") e: RuntimeException) { + internalLogger.log( + level = InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + messageBuilder = { ERROR_HANDLING_BROADCAST_INTENT }, + throwable = e + ) + } + } + + // endregion + + // region SystemInfoProvider + + @SuppressLint("InlinedApi") + override fun register(context: Context) { + registerIntentFilter(context, Intent.ACTION_BATTERY_CHANGED) + registerIntentFilter(context, PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) + } + + override fun unregister(context: Context) { + unregisterReceiver(context) + } + + override fun getLatestSystemInfo(): SystemInfo { + return systemInfo + } + + // endregion + + // region Internal + + private fun registerIntentFilter(context: Context, action: String) { + val filter = IntentFilter() + filter.addAction(action) + registerReceiver(context, filter)?.let { onReceive(context, it) } + } + + private fun handleBatteryIntent(intent: Intent) { + val status = intent.getIntExtra( + BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN + ) + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_UNKNOWN) + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, DEFAULT_BATTERY_SCALE) + val pluggedStatus = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, BATTERY_UNPLUGGED) + val batteryStatus = SystemInfo.BatteryStatus.fromAndroidStatus(status) + val batteryPresent = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true) + + @Suppress("UnsafeThirdPartyFunctionCall") // Not a NaN here + val batteryLevel = ((level * DEFAULT_BATTERY_SCALE.toFloat()) / scale).roundToInt() + val onExternalPowerSource = pluggedStatus in PLUGGED_IN_STATUS_VALUES || !batteryPresent + val batteryFullOrCharging = batteryStatus in batteryFullOrChargingStatus + systemInfo = systemInfo.copy( + batteryFullOrCharging = batteryFullOrCharging, + batteryLevel = batteryLevel, + onExternalPowerSource = onExternalPowerSource + ) + } + + private fun handlePowerSaveIntent(context: Context) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager + val powerSaveMode = powerManager?.isPowerSaveMode ?: false + systemInfo = systemInfo.copy( + powerSaveMode = powerSaveMode + ) + } + + // endregion + + companion object { + + private const val DEFAULT_BATTERY_SCALE = 100 + private const val BATTERY_UNPLUGGED = -1 + private const val BATTERY_LEVEL_UNKNOWN = -1 + private const val ERROR_HANDLING_BROADCAST_INTENT = "Error handling system info broadcast intent." + + private val batteryFullOrChargingStatus = setOf( + SystemInfo.BatteryStatus.CHARGING, + SystemInfo.BatteryStatus.FULL + ) + + private val PLUGGED_IN_STATUS_VALUES = setOf( + BatteryManager.BATTERY_PLUGGED_AC, + BatteryManager.BATTERY_PLUGGED_WIRELESS, + BatteryManager.BATTERY_PLUGGED_USB + ) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BuildSdkVersionProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BuildSdkVersionProvider.kt new file mode 100644 index 0000000000..bf5c49be5b --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BuildSdkVersionProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import com.datadog.android.lint.InternalApi + +/** + * Wrapper around [Build.VERSION.SDK_INT] in order to simplify mocking in tests. + * + * FOR INTERNAL USAGE ONLY. THIS INTERFACE CONTENT MAY CHANGE WITHOUT NOTICE. + */ +@InternalApi +interface BuildSdkVersionProvider { + + /** + * Value of [Build.VERSION.SDK_INT]. + */ + val version: Int + + companion object { + + /** + * Default implementation which calls Build.VERSION under the hood. + */ + val DEFAULT: BuildSdkVersionProvider = object : BuildSdkVersionProvider { + + @ChecksSdkIntAtLeast + override val version: Int = Build.VERSION.SDK_INT + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProvider.kt new file mode 100644 index 0000000000..b9e3d1c640 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProvider.kt @@ -0,0 +1,188 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import android.app.UiModeManager +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.hardware.display.DisplayManager +import android.os.Build +import android.telephony.TelephonyManager +import android.view.Display +import com.datadog.android.api.context.DeviceType +import java.util.Locale +import java.util.TimeZone + +internal class DefaultAndroidInfoProvider( + appContext: Context, + rawDeviceBrand: String, + rawDeviceModel: String, + rawDeviceId: String, + rawOsVersion: String +) : AndroidInfoProvider { + + constructor(appContext: Context) : this( + appContext, + Build.BRAND.orEmpty(), + Build.MODEL.orEmpty(), + Build.ID.orEmpty(), + Build.VERSION.RELEASE.orEmpty() + ) + + // lazy is just to avoid breaking the tests (because without lazy type is resolved at the + // construction time and Build.MODEL is null in unit-tests) and also to have value resolved + // once to avoid different values for foldables during the application lifecycle + override val deviceType: DeviceType by lazy(LazyThreadSafetyMode.PUBLICATION) { + resolveDeviceType(rawDeviceModel, appContext) + } + + override val deviceName: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + if (deviceBrand.isBlank()) { + deviceModel + } else if (deviceModel.contains(deviceBrand)) { + deviceModel + } else { + "$deviceBrand $deviceModel" + } + } + + override val locales: List by lazy(LazyThreadSafetyMode.PUBLICATION) { + val resources = appContext.resources + val languageList = mutableListOf() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val locales = resources.configuration.locales + val localesSize = locales.size() + + for (localeIndex in 0 until localesSize) { + locales[localeIndex]?.toLanguageTag()?.let { + languageList.add(it) + } + } + } else { + @Suppress("DEPRECATION") + resources.configuration.locale?.toLanguageTag()?.let { + languageList.add(it) + } + } + + languageList + } + + override val currentLocale: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + val resources = appContext.resources + val currentLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resources.configuration.locales[0]?.toLanguageTag() + } else { + @Suppress("DEPRECATION") + resources.configuration.locale?.toLanguageTag() + } + + // null shouldn't happen, but if it does this ensures that we return a valid languageTag + currentLocale ?: Locale.getDefault().toLanguageTag() + } + + override val timeZone: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + TimeZone.getDefault().id + } + + override val deviceBrand: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + rawDeviceBrand.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() + } + } + + override val deviceModel: String = rawDeviceModel + + override val deviceBuildId: String = rawDeviceId + + override val osName: String = "Android" + + override val osVersion: String = rawOsVersion + + override val osMajorVersion: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + // result of split always have at least 1 element + @Suppress("UnsafeThirdPartyFunctionCall") + osVersion.split('.').first() + } + + override val architecture: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + System.getProperty("os.arch") ?: "unknown" + } + + override val numberOfDisplays: Int? by lazy(LazyThreadSafetyMode.PUBLICATION) { + val displayManager = appContext.getSystemService(Context.DISPLAY_SERVICE) + as? DisplayManager ?: return@lazy null + + displayManager.displays.count { + it.state !in setOf( + Display.STATE_OFF, + Display.STATE_UNKNOWN + ) + } + } + + companion object { + + const val FEATURE_GOOGLE_ANDROID_TV = "com.google.android.tv" + const val MIN_TABLET_WIDTH_DP = 800 + + private fun resolveDeviceType(model: String, appContext: Context): DeviceType { + return if (isTv(appContext)) { + DeviceType.TV + } else if (isTablet(model, appContext)) { + DeviceType.TABLET + } else if (isMobile(model, appContext)) { + DeviceType.MOBILE + } else { + DeviceType.OTHER + } + } + + private fun isTv(appContext: Context): Boolean { + val uiModeManager = + appContext.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager + if (uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) { + return true + } + + return hasTvFeature(appContext.packageManager) + } + + private fun hasTvFeature( + packageManager: PackageManager + ): Boolean { + return when { + packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) -> true + packageManager.hasSystemFeature(FEATURE_GOOGLE_ANDROID_TV) -> true + else -> false + } + } + + private fun isTablet( + model: String, + appContext: Context + ): Boolean { + with(model.lowercase(Locale.US)) { + if (contains("tablet") || contains("sm-t")) return true + } + return appContext.resources.configuration.smallestScreenWidthDp >= MIN_TABLET_WIDTH_DP + } + + private fun isMobile( + model: String, + appContext: Context + ): Boolean { + if (model.lowercase(Locale.US).contains("phone")) return true + + val telephonyManager = + appContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + return telephonyManager?.phoneType != TelephonyManager.PHONE_TYPE_NONE + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultAppVersionProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultAppVersionProvider.kt new file mode 100644 index 0000000000..27e816e324 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultAppVersionProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import java.util.concurrent.atomic.AtomicReference + +internal class DefaultAppVersionProvider(initialVersion: String) : AppVersionProvider { + + private val value = AtomicReference(initialVersion) + + override var version: String + get() = value.get() + set(value) { + this.value.set(value) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/NoOpAndroidInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/NoOpAndroidInfoProvider.kt new file mode 100644 index 0000000000..85163b6475 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/NoOpAndroidInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import com.datadog.android.api.context.DeviceType + +internal class NoOpAndroidInfoProvider : AndroidInfoProvider { + override val deviceName: String = "" + override val deviceBrand: String = "" + override val deviceModel: String = "" + override val deviceType: DeviceType = DeviceType.MOBILE + override val deviceBuildId: String = "" + override val osName: String = "" + override val osMajorVersion: String = "" + override val osVersion: String = "" + override val architecture: String = "" + override val numberOfDisplays: Int? = null + override val locales: List = emptyList() + override val currentLocale: String = "" + override val timeZone: String = "" +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/NoOpAppVersionProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/NoOpAppVersionProvider.kt new file mode 100644 index 0000000000..5b21d161f7 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/NoOpAppVersionProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +internal class NoOpAppVersionProvider : AppVersionProvider { + @Suppress("UNUSED_PARAMETER") + override var version: String + get() = "" + set(value) {} +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfo.kt similarity index 88% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfo.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfo.kt index bf9b2212cb..4565465923 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfo.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfo.kt @@ -9,9 +9,10 @@ package com.datadog.android.core.internal.system import android.os.BatteryManager internal data class SystemInfo( - val batteryStatus: BatteryStatus = BatteryStatus.UNKNOWN, + val batteryFullOrCharging: Boolean = false, val batteryLevel: Int = -1, - val powerSaveMode: Boolean = false + val powerSaveMode: Boolean = false, + val onExternalPowerSource: Boolean = false ) { internal enum class BatteryStatus { diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfoProvider.kt similarity index 100% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfoProvider.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/SystemInfoProvider.kt diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/BackPressureExecutorService.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/BackPressureExecutorService.kt new file mode 100644 index 0000000000..a00fcd59a1 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/BackPressureExecutorService.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.core.thread.FlushableExecutorService +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +/** + * A single threaded executor service using a BackPressureStrategy. + */ +internal class BackPressureExecutorService( + val logger: InternalLogger, + executorContext: String, + backpressureStrategy: BackPressureStrategy +) : ThreadPoolExecutor( + CORE_POOL_SIZE, + CORE_POOL_SIZE, + THREAD_POOL_MAX_KEEP_ALIVE_MS, + TimeUnit.MILLISECONDS, + BackPressuredBlockingQueue(logger, executorContext, backpressureStrategy), + DatadogThreadFactory(executorContext) +), + FlushableExecutorService { + + // region FlushableExecutorService + + @Suppress("TooGenericExceptionCaught") + override fun drainTo(destination: MutableCollection) { + try { + queue.drainTo(destination) + } catch (e: IllegalArgumentException) { + onDrainException(e) + } catch (e: NullPointerException) { + onDrainException(e) + } catch (e: UnsupportedOperationException) { + onDrainException(e) + } catch (e: ClassCastException) { + onDrainException(e) + } + } + + // endregion + + // region ThreadPoolExecutor + + override fun afterExecute(r: Runnable?, t: Throwable?) { + super.afterExecute(r, t) + loggingAfterExecute(r, t, logger) + } + + // endregion + + private fun onDrainException(e: RuntimeException) { + logger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { "Unable to drain BackPressureExecutorService queue" }, + e + ) + } + + companion object { + private const val CORE_POOL_SIZE = 1 + private val THREAD_POOL_MAX_KEEP_ALIVE_MS = TimeUnit.SECONDS.toMillis(5) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/BackPressuredBlockingQueue.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/BackPressuredBlockingQueue.kt new file mode 100644 index 0000000000..73f8f464ce --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/BackPressuredBlockingQueue.kt @@ -0,0 +1,156 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureMitigation +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.internal.thread.NamedExecutionUnit +import java.util.concurrent.TimeUnit + +/** + * [LinkedBlockingQueue] that supports backpressure handling via the chosen backpressure mitigation strategy. + * + * This queue may be either bounded or unbounded by specifying capacity. See docs of [LinkedBlockingQueue] for more + * details. + * + * If queue is unbounded, there is still a possibility to be notified if certain size threshold is reached. + */ +internal class BackPressuredBlockingQueue : ObservableLinkedBlockingQueue { + + private val logger: InternalLogger + private val executorContext: String + internal val capacity: Int + private val notifyThreshold: Int + private val onThresholdReached: () -> Unit + private val onItemDropped: (Any) -> Unit + private val backpressureMitigation: BackPressureMitigation? + + constructor( + logger: InternalLogger, + executorContext: String, + backPressureStrategy: BackPressureStrategy + ) : this( + logger, + executorContext, + backPressureStrategy.capacity, + backPressureStrategy.capacity, + backPressureStrategy.onThresholdReached, + backPressureStrategy.onItemDropped, + backPressureStrategy.backpressureMitigation + ) + + constructor( + logger: InternalLogger, + executorContext: String, + notifyThreshold: Int, + capacity: Int, + onThresholdReached: () -> Unit, + onItemDropped: (Any) -> Unit, + backpressureMitigation: BackPressureMitigation? + ) : super(capacity) { + this.logger = logger + this.executorContext = executorContext + this.capacity = capacity + this.notifyThreshold = notifyThreshold + this.onThresholdReached = onThresholdReached + this.onItemDropped = onItemDropped + this.backpressureMitigation = backpressureMitigation + } + + override fun offer(e: E): Boolean { + return addWithBackPressure(e) { + @Suppress("UnsafeThirdPartyFunctionCall") // can't have NPE here + super.offer(it) + } + } + + override fun offer(e: E, timeout: Long, unit: TimeUnit?): Boolean { + @Suppress("UnsafeThirdPartyFunctionCall") // can't have NPE here + val accepted = super.offer(e, timeout, unit) + if (!accepted) { + return offer(e) + } else { + if (size == notifyThreshold) { + notifyThresholdReached() + } + return true + } + } + + override fun put(e: E) { + if (size + 1 == notifyThreshold) { + notifyThresholdReached() + } + super.put(e) + } + + private fun addWithBackPressure( + e: E, + operation: (E) -> Boolean + ): Boolean { + val remainingCapacity = remainingCapacity() + return if (remainingCapacity == 0) { + when (backpressureMitigation) { + BackPressureMitigation.DROP_OLDEST -> { + val first = take() + notifyItemDropped(first) + operation(e) + } + + BackPressureMitigation.IGNORE_NEWEST, null -> { + notifyItemDropped(e) + true + } + } + } else { + if (size + 1 == notifyThreshold) { + notifyThresholdReached() + } + operation(e) + } + } + + private fun notifyThresholdReached() { + val dump = dumpQueue() + val backPressureMap = buildMap { + put("capacity", capacity) + if (!dump.isNullOrEmpty()) { + put("dump", dump) + } + } + onThresholdReached() + logger.log( + level = InternalLogger.Level.WARN, + targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + messageBuilder = { "BackPressuredBlockingQueue reached capacity:$notifyThreshold" }, + throwable = null, + onlyOnce = false, + additionalProperties = mapOf( + "backpressure" to backPressureMap, + "executor.context" to executorContext + ) + ) + } + + private fun notifyItemDropped(item: E) { + onItemDropped(item) + val name = (item as? NamedExecutionUnit)?.name ?: item.toString() + // Note, do not send this to telemetry as it might cause a stack overflow + logger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { "Dropped item in BackPressuredBlockingQueue queue: $name" }, + throwable = null, + onlyOnce = false, + additionalProperties = mapOf( + "backpressure.capacity" to capacity, + "executor.context" to executorContext + ) + ) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/DatadogThreadFactory.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/DatadogThreadFactory.kt new file mode 100644 index 0000000000..58bf53b17e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/DatadogThreadFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicInteger + +internal class DatadogThreadFactory( + private val newThreadContext: String +) : ThreadFactory { + + private val threadNumber = AtomicInteger(1) + + override fun newThread(r: Runnable?): Thread { + val index = threadNumber.getAndIncrement() + val threadName = "datadog-$newThreadContext-thread-$index" + + @Suppress("UnsafeThirdPartyFunctionCall") // both arguments are safe + val thread = Thread(r, threadName) + thread.priority = Thread.NORM_PRIORITY + thread.isDaemon = false + return thread + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor.kt new file mode 100644 index 0000000000..ec7f712b07 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureStrategy +import java.util.concurrent.RejectedExecutionHandler +import java.util.concurrent.ScheduledThreadPoolExecutor + +/** + * [ScheduledThreadPoolExecutor] with a [ScheduledThreadPoolExecutor.afterExecute] hook, + * which will log any unhandled exception raised. + * + * @param corePoolSize see [ScheduledThreadPoolExecutor] docs. + * @param executorContext Context to be used for logging and naming threads running on this executor. + * @param logger [InternalLogger] instance. + * @param backPressureStrategy the back pressure strategy to notify dropped items + */ +// TODO RUM-3704 create an implementation that uses the backpressure strategy +internal class LoggingScheduledThreadPoolExecutor( + corePoolSize: Int, + executorContext: String, + private val logger: InternalLogger, + private val backPressureStrategy: BackPressureStrategy +) : ScheduledThreadPoolExecutor( + corePoolSize, + DatadogThreadFactory(executorContext), + RejectedExecutionHandler { r, _ -> + if (r != null) { + logger.log( + level = InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + messageBuilder = { "Dropped scheduled item in LoggingScheduledThreadPoolExecutor queue: $r" }, + throwable = null, + onlyOnce = false, + additionalProperties = mapOf("executor.context" to executorContext) + ) + backPressureStrategy.onItemDropped(r) + } + } +) { + + /** @inheritdoc */ + override fun afterExecute(r: Runnable?, t: Throwable?) { + super.afterExecute(r, t) + loggingAfterExecute(r, t, logger) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ObservableLinkedBlockingQueue.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ObservableLinkedBlockingQueue.kt new file mode 100644 index 0000000000..8402dc322e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ObservableLinkedBlockingQueue.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.internal.thread.NamedExecutionUnit +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong + +internal open class ObservableLinkedBlockingQueue : LinkedBlockingQueue { + + private val currentTimeProvider: () -> Long + + constructor( + currentTimeProvider: () -> Long = { System.currentTimeMillis() } + ) : this(Int.MAX_VALUE, currentTimeProvider) + + constructor( + capacity: Int, + currentTimeProvider: () -> Long = { System.currentTimeMillis() } + ) : super(capacity) { + this.currentTimeProvider = currentTimeProvider + } + + private val lastDumpTimestamp: AtomicLong = AtomicLong(0) + + fun dumpQueue(): Map? { + val currentTime = currentTimeProvider.invoke() + val last = lastDumpTimestamp.get() + val timeSinceLastDump = currentTime - last + return if (timeSinceLastDump > DUMPING_TIME_INTERVAL_IN_MS) { + if (lastDumpTimestamp.compareAndSet(last, currentTime)) { + buildDumpMap() + } else { + null + } + } else { + null + } + } + + private fun buildDumpMap(): Map { + val map = mutableMapOf() + super.toArray().forEach { runnable -> + (runnable as? NamedExecutionUnit)?.name?.let { + map.put(it, (map[it] ?: 0) + 1) + } + } + return map + } + + companion object { + private val DUMPING_TIME_INTERVAL_IN_MS = TimeUnit.SECONDS.toMillis(5) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory.kt new file mode 100644 index 0000000000..cf4a7a970b --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureStrategy +import java.util.concurrent.ScheduledExecutorService + +/** + * A factory for [ScheduledExecutorService]. + */ +internal fun interface ScheduledExecutorServiceFactory { + + /** + * Create an instance of [ScheduledExecutorService]. + * @param internalLogger the internal logger + * @param executorContext Context to be used for logging and naming threads running on this executor. + * @param backPressureStrategy the strategy to handle back-pressure + * @return the instance + */ + fun create( + internalLogger: InternalLogger, + executorContext: String, + backPressureStrategy: BackPressureStrategy + ): ScheduledExecutorService +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ThreadExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ThreadExt.kt new file mode 100644 index 0000000000..b604e9ace7 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ThreadExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future + +/** + * A utility method to perform a Thread.sleep() safely. + * @return whether the thread was interrupted during the sleep + */ +@Suppress("ReturnCount") +internal fun sleepSafe(durationMs: Long, internalLogger: InternalLogger): Boolean { + try { + Thread.sleep(durationMs) + return false + } catch (e: InterruptedException) { + try { + // Restore the interrupted status + Thread.currentThread().interrupt() + } catch (se: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Thread was unable to set its own interrupted state" }, + se + ) + } + return true + } catch (e: IllegalArgumentException) { + // This means we tried sleeping for a negative time + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Thread tried to sleep for a negative amount of time" }, + e + ) + return false + } +} + +/** + * Logs any exception raised during the execution. Tested indirectly using + * the tests of [BackPressureExecutorService] and [LoggingScheduledThreadPoolExecutor]. + */ +internal fun loggingAfterExecute(task: Runnable?, t: Throwable?, logger: InternalLogger) { + var throwable = t + if (t == null && task is Future<*> && task.isDone) { + try { + task.get() + } catch (ce: CancellationException) { + throwable = ce + } catch (ee: ExecutionException) { + throwable = ee.cause + } catch (ie: InterruptedException) { + // ignore/reset + try { + Thread.currentThread().interrupt() + } catch (se: SecurityException) { + // this should not happen + logger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Thread was unable to set its own interrupted state" }, + se + ) + } + } + } + if (throwable != null) { + logger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { ERROR_UNCAUGHT_EXECUTION_EXCEPTION }, + throwable + ) + } +} + +internal const val ERROR_UNCAUGHT_EXECUTION_EXCEPTION = + "Uncaught exception during the task execution" diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExt.kt new file mode 100644 index 0000000000..7def26113d --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +internal const val MAX_SLEEP_DURATION_IN_MS = 10L + +internal fun ThreadPoolExecutor.waitToIdle(timeoutInMs: Long, internalLogger: InternalLogger): Boolean { + val startTime = System.nanoTime() + val timeoutInNs = TimeUnit.MILLISECONDS.toNanos(timeoutInMs) + val sleepDurationInMs = timeoutInMs.coerceIn(0, MAX_SLEEP_DURATION_IN_MS) + var interrupted: Boolean + do { + if (isIdle()) { + return true + } + interrupted = sleepSafe(sleepDurationInMs, internalLogger) + } while (((System.nanoTime() - startTime) < timeoutInNs) && !interrupted) + + return isIdle() +} + +internal fun ThreadPoolExecutor.isIdle(): Boolean { + return (this.taskCount - this.completedTaskCount <= 0) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/AppStartTimeProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/AppStartTimeProvider.kt new file mode 100644 index 0000000000..3826d781b7 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/AppStartTimeProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.time + +internal interface AppStartTimeProvider { + /** + * Provide the time the application started in nanoseconds from device boot, or our best guess + * if the actual start time is not available. + */ + val appStartTimeNs: Long +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DatadogNtpEndpoint.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DatadogNtpEndpoint.kt new file mode 100644 index 0000000000..f9102057da --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DatadogNtpEndpoint.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.time + +/** + * This object contains constant values for all the Datadog NTP Endpoint urls used in the SDK. + */ +internal enum class DatadogNtpEndpoint(val host: String) { + + /** + * Endpoint for the Network Time Protocol time syncing. + */ + NTP_0("0.datadog.pool.ntp.org"), + + /** + * Endpoint for the Network Time Protocol time syncing. + */ + NTP_1("1.datadog.pool.ntp.org"), + + /** + * Endpoint for the Network Time Protocol time syncing. + */ + NTP_2("2.datadog.pool.ntp.org"), + + /** + * Endpoint for the Network Time Protocol time syncing. + */ + NTP_3("3.datadog.pool.ntp.org") +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProvider.kt new file mode 100644 index 0000000000..1209ec2130 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.time + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Process +import android.os.SystemClock +import com.datadog.android.core.internal.system.BuildSdkVersionProvider +import com.datadog.android.rum.DdRumContentProvider +import java.util.concurrent.TimeUnit + +internal class DefaultAppStartTimeProvider( + buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT +) : AppStartTimeProvider { + + override val appStartTimeNs: Long by lazy(LazyThreadSafetyMode.PUBLICATION) { + @SuppressLint("NewApi") + when { + buildSdkVersionProvider.version >= Build.VERSION_CODES.N -> { + val diffMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime() + System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(diffMs) + } + else -> DdRumContentProvider.createTimeNs + } + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt similarity index 80% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt index 3b4bc74a8e..809c47706c 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt @@ -6,6 +6,7 @@ package com.datadog.android.core.internal.time +import com.datadog.android.internal.time.TimeProvider import com.lyft.kronos.Clock import java.util.concurrent.TimeUnit @@ -21,10 +22,14 @@ internal class KronosTimeProvider( return clock.getCurrentTimeMs() } - override fun getServerOffsetNanos(): Long { + override fun getServerOffsetMillis(): Long { val server = clock.getCurrentTimeMs() val device = System.currentTimeMillis() val delta = server - device - return TimeUnit.MILLISECONDS.toNanos(delta) + return delta + } + + override fun getServerOffsetNanos(): Long { + return TimeUnit.MILLISECONDS.toNanos(getServerOffsetMillis()) } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/LoggingSyncListener.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/LoggingSyncListener.kt new file mode 100644 index 0000000000..ba28ace5e2 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/LoggingSyncListener.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.time + +import com.datadog.android.api.InternalLogger +import com.lyft.kronos.SyncListener + +internal class LoggingSyncListener(private val internalLogger: InternalLogger) : SyncListener { + override fun onStartSync(host: String) { + // no-op + } + + override fun onSuccess(ticksDelta: Long, responseTimeMs: Long) { + // no-op + } + + override fun onError(host: String, throwable: Throwable) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Kronos onError @host:$host" }, + throwable + ) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/DatadogUserInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/DatadogUserInfoProvider.kt new file mode 100644 index 0000000000..47919e3bee --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/DatadogUserInfoProvider.kt @@ -0,0 +1,44 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.user + +import com.datadog.android.api.context.UserInfo + +internal class DatadogUserInfoProvider : MutableUserInfoProvider { + + @Volatile + private var internalUserInfo = UserInfo() + + override fun setUserInfo(id: String, name: String?, email: String?, extraInfo: Map) { + internalUserInfo = internalUserInfo.copy( + id = id, + name = name, + email = email, + additionalProperties = extraInfo.toMap() + ) + } + + override fun setAnonymousId(id: String?) { + internalUserInfo = internalUserInfo.copy( + anonymousId = id + ) + } + + override fun addUserProperties(properties: Map) { + internalUserInfo = internalUserInfo.copy( + additionalProperties = internalUserInfo.additionalProperties + properties + ) + } + + override fun clearUserInfo() { + internalUserInfo = UserInfo(internalUserInfo.anonymousId) + } + + override fun getUserInfo(): UserInfo { + return internalUserInfo + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/MutableUserInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/MutableUserInfoProvider.kt new file mode 100644 index 0000000000..2a12ac7eb4 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/MutableUserInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.user + +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface MutableUserInfoProvider : UserInfoProvider { + + fun setUserInfo( + id: String, + name: String?, + email: String?, + extraInfo: Map + ) + + fun setAnonymousId(id: String?) + + fun addUserProperties(properties: Map) + + fun clearUserInfo() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/UserInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/UserInfoProvider.kt new file mode 100644 index 0000000000..b9a69bc3ad --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/user/UserInfoProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.user + +import com.datadog.android.api.context.UserInfo +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface UserInfoProvider { + + fun getUserInfo(): UserInfo +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ByteArrayExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ByteArrayExt.kt new file mode 100644 index 0000000000..c28d18664c --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ByteArrayExt.kt @@ -0,0 +1,179 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.datadog.android.api.InternalLogger +import com.datadog.android.lint.InternalApi +import java.nio.ByteBuffer + +/** + * Splits this [ByteArray] to a list of [ByteArray] around occurrences of the specified [delimiter]. + * + * @param delimiter a byte to be used as delimiter. + * @param internalLogger logger to use. + */ +internal fun ByteArray.split(delimiter: Byte, internalLogger: InternalLogger): List { + val result = mutableListOf() + + var offset = 0 + var nextIndex: Int + + do { + nextIndex = indexOf(delimiter, offset) + val length = if (nextIndex >= 0) nextIndex - offset else size - offset + if (length > 0) { + val subArray = ByteArray(length) + this.copyTo(offset, subArray, 0, length, internalLogger) + result.add(subArray) + } + offset = nextIndex + 1 + } while (nextIndex != -1) + + return result +} + +/** + * Joins a collection of [ByteArray] elements into a single [ByteArray], taking into account + * separator between elements and prefix and suffix decoration of the final array. + * + * @param separator Separator to use between the parts joined. + * @param prefix Optional prefix to add to the result. + * @param suffix Optional suffix to add to the result. + * @param internalLogger logger to use. + */ +@InternalApi +fun Collection.join( + separator: ByteArray, + prefix: ByteArray = ByteArray(0), + suffix: ByteArray = ByteArray(0), + internalLogger: InternalLogger +): ByteArray { + val dataSize = this.sumOf { it.size } + val separatorsSize = if (this.isNotEmpty()) separator.size * (this.size - 1) else 0 + val resultSize = prefix.size + dataSize + separatorsSize + suffix.size + + val result = ByteArray(resultSize) + + var offset = 0 + + prefix.copyTo(0, result, 0, prefix.size, internalLogger) + offset += prefix.size + + for (item in this.withIndex()) { + item.value.copyTo(0, result, offset, item.value.size, internalLogger) + offset += item.value.size + if (item.index != this.size - 1) { + separator.copyTo(0, result, offset, separator.size, internalLogger) + offset += separator.size + } + } + + suffix.copyTo(0, result, offset, suffix.size, internalLogger) + + return result +} + +/** + * Returns the index within this [ByteArray] of the first occurrence of the specified [b], + * starting from the specified [startIndex]. + * + * @return An index of the first occurrence of [b] or `-1` if none is found. + */ +@Suppress("ReturnCount") +internal fun ByteArray.indexOf(b: Byte, startIndex: Int = 0): Int { + if (startIndex < 0) return -1 + + for (i in startIndex until size) { + @Suppress("UnsafeThirdPartyFunctionCall") // iteration over indexes which exist + if (get(i) == b) { + return i + } + } + return -1 +} + +/** + * Performs a safe version of [System.arraycopy] by performing the necessary checks and try-catch. + * + * @return true if the copy was successful. + */ +internal fun ByteArray.copyTo( + srcPos: Int, + dest: ByteArray, + destPos: Int, + length: Int, + internalLogger: InternalLogger +): Boolean { + return if (destPos + length > dest.size) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Cannot copy ByteArray, dest doesn't have enough space" } + ) + false + } else if (srcPos + length > size) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Cannot copy ByteArray, src doesn't have enough data" } + ) + false + } else { + // this and dest can't be null, NPE cannot happen here + // both are ByteArrays, ArrayStoreException cannot happen here + @Suppress("UnsafeThirdPartyFunctionCall") + System.arraycopy(this, srcPos, dest, destPos, length) + true + } +} + +/** + * Reads a long from this byte array. + * Note that the ByteArray needs to be at least of size 8. + */ +internal fun ByteArray.toLong(): Long { + // wrap provides valid backing array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.wrap(this).getLong() +} + +/** + * Reads an int from this byte array. + * Note that the ByteArray needs to be at least of size 4. + */ +internal fun ByteArray.toInt(): Int { + // wrap provides valid backing array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.wrap(this).getInt() +} + +/** + * Reads a short from this byte array. + * Note that the ByteArray needs to be at least of size 2. + */ +internal fun ByteArray.toShort(): Short { + // wrap provides valid backing array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.wrap(this).getShort() +} + +/** + * Creates a copy of a range within this ByteArray into a new Byte Array. + * If the copy would have thrown an exception, an empty byte array is returned instead. + * @param fromIndex the start of the range (inclusive) to copy. + * @param toIndex the end of the range (exclusive) to copy. + */ +@Suppress("TooGenericExceptionCaught", "SwallowedException") +internal fun ByteArray.copyOfRangeSafe(fromIndex: Int, toIndex: Int): ByteArray { + return try { + this.copyOfRange(fromIndex, toIndex) + } catch (e: IndexOutOfBoundsException) { + byteArrayOf() + } catch (e: IllegalArgumentException) { + byteArrayOf() + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ConcurrencyExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ConcurrencyExt.kt new file mode 100644 index 0000000000..e98ef91509 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ConcurrencyExt.kt @@ -0,0 +1,184 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import androidx.annotation.CheckResult +import com.datadog.android.api.InternalLogger +import com.datadog.android.lint.InternalApi +import java.util.Locale +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +internal const val ERROR_TASK_REJECTED = "Unable to schedule %s task on the executor" +internal const val ERROR_FUTURE_GET_FAILED = "Unable to get result of the %s task" + +/** + * Executes [Runnable] without throwing [RejectedExecutionException] if it cannot be accepted + * for execution. + * + * @param operationName Name of the task. + * @param internalLogger Internal logger. + * @param runnable Task to run. + */ +@InternalApi +fun Executor.executeSafe( + operationName: String, + internalLogger: InternalLogger, + runnable: Runnable +) { + try { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + execute(runnable) + } catch (e: RejectedExecutionException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_TASK_REJECTED.format(Locale.US, operationName) }, + e + ) + } +} + +/** + * Executes [Runnable] without throwing [RejectedExecutionException] if it cannot be accepted + * for execution. + * + * @param operationName Name of the task. + * @param delay Task scheduling delay. + * @param unit Delay unit. + * @param internalLogger Internal logger. + * @param runnable Task to run. + */ +@InternalApi +fun ScheduledExecutorService.scheduleSafe( + operationName: String, + delay: Long, + unit: TimeUnit, + internalLogger: InternalLogger, + runnable: Runnable +): ScheduledFuture<*>? { + return try { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + schedule(runnable, delay, unit) + } catch (e: RejectedExecutionException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_TASK_REJECTED.format(Locale.US, operationName) }, + e + ) + null + } +} + +/** + * Submit [Runnable] without throwing [RejectedExecutionException] if it cannot be accepted + * for execution. + * + * @param operationName Name of the task. + * @param internalLogger Internal logger. + * @param runnable Task to run. + */ +@InternalApi +@CheckResult +fun ExecutorService.submitSafe( + operationName: String, + internalLogger: InternalLogger, + runnable: Runnable +): Future<*>? { + return try { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + submit(runnable) + } catch (e: RejectedExecutionException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_TASK_REJECTED.format(Locale.US, operationName) }, + e + ) + null + } +} + +/** + * Submit [Callable] without throwing [RejectedExecutionException] if it cannot be accepted + * for execution. + * + * @param T Task result type. + * @param operationName Name of the task. + * @param internalLogger Internal logger. + * @param callable Task to run. + */ +@InternalApi +@CheckResult +fun ExecutorService.submitSafe( + operationName: String, + internalLogger: InternalLogger, + callable: Callable +): Future? { + return try { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + submit(callable) + } catch (e: RejectedExecutionException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_TASK_REJECTED.format(Locale.US, operationName) }, + e + ) + null + } +} + +/** + * Safely unwraps [Future] result without throwing any exception. + * + * @param T Task result type. + * @param operationName Name of the task. + * @param internalLogger Internal logger. + */ +@InternalApi +fun Future?.getSafe( + operationName: String, + internalLogger: InternalLogger +): T? { + return try { + this?.get() + } catch (e: InterruptedException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { ERROR_FUTURE_GET_FAILED.format(Locale.US, operationName) }, + e + ) + null + } catch (e: CancellationException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { ERROR_FUTURE_GET_FAILED.format(Locale.US, operationName) }, + e + ) + null + } catch (e: ExecutionException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { ERROR_FUTURE_GET_FAILED.format(Locale.US, operationName) }, + e + ) + null + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/FileLockExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/FileLockExt.kt similarity index 78% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/FileLockExt.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/FileLockExt.kt index bf17349102..7888891cc8 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/FileLockExt.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/FileLockExt.kt @@ -6,8 +6,11 @@ package com.datadog.android.core.internal.utils +import java.io.IOException import java.nio.channels.FileLock +@Throws(IOException::class) +@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block internal inline fun FileLock.use(block: (FileLock) -> R): R { try { return block(this) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/MapUtils.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/MapUtils.kt new file mode 100644 index 0000000000..af7d7bd475 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/MapUtils.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +/** + * @return a new mutable map containing all key-value pairs from the given array of pairs. + * + * The returned map preserves the entry iteration order of the original array. + * If any of two pairs would have the same key the last one gets added to the map. + */ +internal fun Iterable>.toMutableMap(): MutableMap { + return toMap(mutableMapOf()) +} + +/** + * @return the [MutableMap] if its not `null`, or the empty [MutableMap] otherwise. + */ +internal fun MutableMap?.orEmpty(): MutableMap { + return this ?: mutableMapOf() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt new file mode 100644 index 0000000000..2663481ea3 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.NULL_MAP_VALUE +import com.datadog.android.lint.InternalApi +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import org.json.JSONArray +import org.json.JSONObject +import java.util.Date +import java.util.Locale + +internal fun retryWithDelay( + times: Int, + retryDelayNs: Long, + internalLogger: InternalLogger, + block: () -> Boolean +): Boolean { + return retryWithDelay(block, times, retryDelayNs, internalLogger) +} + +@Suppress("TooGenericExceptionCaught") +internal inline fun retryWithDelay( + block: () -> Boolean, + times: Int, + loopsDelayInNanos: Long, + internalLogger: InternalLogger +): Boolean { + var retryCounter = 1 + var wasSuccessful = false + var loopTimeOrigin = System.nanoTime() - loopsDelayInNanos + while (retryCounter <= times && !wasSuccessful) { + if ((System.nanoTime() - loopTimeOrigin) >= loopsDelayInNanos) { + wasSuccessful = try { + block() + } catch (e: Exception) { + internalLogger.log( + InternalLogger.Level.ERROR, + targets = listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { "Internal I/O operation failed" }, + e + ) + false + } + loopTimeOrigin = System.nanoTime() + retryCounter++ + } + } + return wasSuccessful +} + +@Suppress("UndocumentedPublicClass") +@InternalApi +object JsonSerializer { + // it could be an extension function, but since the scope is very wide (Any?) in order to avoid + // polluting user-space, we are going to encapsulate it. Maybe later if we have an internal + // package it can be converted back to the extension function. + + internal const val ITEM_SERIALIZATION_ERROR = "Error serializing value for key %s, value was dropped." + + /** + * Converts arbitrary object to the [JsonElement] with the best effort. [Any.toString] is + * used as a fallback. + */ + @InternalApi + fun toJsonElement(item: Any?): JsonElement { + return when (item) { + NULL_MAP_VALUE -> JsonNull.INSTANCE + null -> JsonNull.INSTANCE + JsonNull.INSTANCE -> JsonNull.INSTANCE + is Boolean -> JsonPrimitive(item) + is Int -> JsonPrimitive(item) + is Long -> JsonPrimitive(item) + is Float -> JsonPrimitive(item) + is Double -> JsonPrimitive(item) + is String -> JsonPrimitive(item) + is Date -> JsonPrimitive(item.time) + // this line should come before Iterable, otherwise this branch is never executed + is JsonArray -> item + is Iterable<*> -> item.toJsonArray() + is Map<*, *> -> item.toJsonObject() + is JsonObject -> item + is JsonPrimitive -> item + is JSONObject -> item.toJsonObject() + is JSONArray -> item.toJsonArray() + else -> JsonPrimitive(item.toString()) + } + } + + /** + * This method will convert all values to JSON in a safe way, meaning if serialization fails + * for the particular value, the process will continue and faulty value will be dropped. + */ + @InternalApi + fun Map.safeMapValuesToJson(internalLogger: InternalLogger): Map { + val result = mutableMapOf() + forEach { + try { + result += it.key to toJsonElement(it.value) + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + internalLogger.log( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + messageBuilder = { ITEM_SERIALIZATION_ERROR.format(Locale.US, it.key) }, + throwable = e + ) + } + } + return result + } +} + +internal fun Any?.fromJsonElement(): Any? { + return when (this) { + is JsonNull -> null + is JsonPrimitive -> { + if (this.isBoolean) { + this.asBoolean + } else if (this.isNumber) { + this.asNumber + } else if (this.isString) { + this.asString + } else { + this + } + } + + is JsonObject -> this.asDeepMap() + + else -> this + } +} + +internal fun Iterable<*>.toJsonArray(): JsonElement { + val array = JsonArray() + forEach { + array.add(JsonSerializer.toJsonElement(it)) + } + return array +} + +internal fun Map<*, *>.toJsonObject(): JsonElement { + val obj = JsonObject() + forEach { + obj.add(it.key.toString(), JsonSerializer.toJsonElement(it.value)) + } + return obj +} + +internal fun JSONObject.toJsonObject(): JsonElement { + val obj = JsonObject() + for (key in keys()) { + @Suppress("UnsafeThirdPartyFunctionCall") // iteration over keys which exist + obj.add(key, JsonSerializer.toJsonElement(get(key))) + } + return obj +} + +internal fun JSONArray.toJsonArray(): JsonElement { + val obj = JsonArray() + for (index in 0 until length()) { + @Suppress("UnsafeThirdPartyFunctionCall") // iteration over indexes which exist + obj.add(JsonSerializer.toJsonElement(get(index))) + } + return obj +} + +internal fun JsonObject.asDeepMap(): Map { + val map = mutableMapOf() + entrySet().forEach { + map[it.key] = it.value.fromJsonElement() + } + return map +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/NumberExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/NumberExt.kt new file mode 100644 index 0000000000..04347f8983 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/NumberExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import java.nio.ByteBuffer + +/** + * Converts this [Short] into a [ByteArray] representation. + */ +internal fun Short.toByteArray(): ByteArray { + // capacity is not a negative integer, buffer is not read only, + // has sufficient capacity and is backed by an array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.allocate(Short.SIZE_BYTES).putShort(this).array() +} + +/** + * Converts this [Int] into a [ByteArray] representation. + */ +internal fun Int.toByteArray(): ByteArray { + // capacity is not a negative integer, buffer is not read only, + // has sufficient capacity and is backed by an array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.allocate(Int.SIZE_BYTES).putInt(this).array() +} + +/** + * Converts this [Long] into a [ByteArray] representation. + */ +internal fun Long.toByteArray(): ByteArray { + // capacity is not a negative integer, buffer is not read only, + // has sufficient capacity and is backed by an array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.allocate(Long.SIZE_BYTES).putLong(this).array() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/RuntimeUtils.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/RuntimeUtils.kt new file mode 100644 index 0000000000..8a837d00fe --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/RuntimeUtils.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.datadog.android.api.InternalLogger + +// Use it only when there is no way to access the SDK-specific logger. +internal var unboundInternalLogger: InternalLogger = InternalLogger.UNBOUND diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtils.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtils.kt new file mode 100644 index 0000000000..281e832795 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtils.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import android.content.Context +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.UploadWorker +import java.util.concurrent.TimeUnit + +internal const val CANCEL_ERROR_MESSAGE = "Error cancelling the UploadWorker" +internal const val SETUP_ERROR_MESSAGE = "Error while trying to setup the UploadWorker" +internal const val UPLOAD_WORKER_WAS_SCHEDULED = "UploadWorker was scheduled." +internal const val UPLOAD_WORKER_NAME = "DatadogUploadWorker" +internal const val TAG_DATADOG_UPLOAD = "DatadogBackgroundUpload" + +internal const val DELAY_MS: Long = 5000 + +internal fun cancelUploadWorker( + context: Context, + instanceName: String, + internalLogger: InternalLogger +) { + try { + val workManager = WorkManager.getInstance(context) + workManager.cancelAllWorkByTag("$TAG_DATADOG_UPLOAD/$instanceName") + } catch (e: IllegalStateException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { CANCEL_ERROR_MESSAGE }, + e + ) + } +} + +@Suppress("TooGenericExceptionCaught") +internal fun triggerUploadWorker( + context: Context, + instanceName: String, + internalLogger: InternalLogger +) { + try { + val workManager = WorkManager.getInstance(context) + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.NOT_ROAMING) + .build() + val uploadWorkRequest = OneTimeWorkRequest.Builder(UploadWorker::class.java) + .setConstraints(constraints) + .addTag("$TAG_DATADOG_UPLOAD/$instanceName") + .setInitialDelay(DELAY_MS, TimeUnit.MILLISECONDS) + .setInputData(Data.Builder().putString(UploadWorker.DATADOG_INSTANCE_NAME, instanceName).build()) + .build() + workManager.enqueueUniqueWork( + UPLOAD_WORKER_NAME, + ExistingWorkPolicy.REPLACE, + uploadWorkRequest + ) + internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.MAINTAINER, + { UPLOAD_WORKER_WAS_SCHEDULED } + ) + } catch (e: Exception) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { SETUP_ERROR_MESSAGE }, + e + ) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/MethodCallSamplingRate.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/MethodCallSamplingRate.kt new file mode 100644 index 0000000000..b871939db3 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/MethodCallSamplingRate.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.metrics + +/** + * Sampling rates for Method Call telemetry. + * @param rate the rate to sample at (between 0 and 100). + */ +enum class MethodCallSamplingRate(val rate: Float) { + /** + * Sample all. + */ + ALL(rate = 100.0f), + + /** + * Sample 10% of the time. + */ + HIGH(rate = 10.0f), + + /** + * Sample 1% of the time. + */ + MEDIUM(rate = 1.0f), + + /** + * Sample 0.1% of the time. + */ + LOW(rate = 0.1f), + + /** + * Sample 0.01% of the time. + */ + REDUCED(rate = 0.01f), + + /** + * Sample 0.001% of the time. + */ + RARE(rate = 0.001f) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/PerformanceMetric.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/PerformanceMetric.kt new file mode 100644 index 0000000000..789f1a3774 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/PerformanceMetric.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.metrics + +import com.datadog.android.lint.InternalApi + +/** + * Base class for performance metric events. + */ +interface PerformanceMetric { + /** + * Stops measuring and sends the performance metric. + * @param isSuccessful - was the operation being measured completed successfully. + */ + @InternalApi + fun stopAndSend(isSuccessful: Boolean) + + companion object { + /** + * Basic Metric type key. + */ + const val METRIC_TYPE: String = "metric_type" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/TelemetryMetricType.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/TelemetryMetricType.kt new file mode 100644 index 0000000000..df19ee1d96 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/metrics/TelemetryMetricType.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.metrics + +/** + * Types of performance metrics. + */ +enum class TelemetryMetricType { + /** + * "method called" performance metric. + */ + MethodCalled +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/PersistenceStrategy.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/PersistenceStrategy.kt new file mode 100644 index 0000000000..d92af10be8 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/PersistenceStrategy.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.persistence + +import androidx.annotation.WorkerThread +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.tools.annotation.NoOpImplementation + +/** + * The main strategy used to persist data between the moment it's tracked and created, + * and the moment it's uploaded to the intake. + */ +@NoOpImplementation +interface PersistenceStrategy { + + /** + * A factory used to create an instance of a [PersistenceStrategy]. + * + * Each instance of a persistence strategy should have independent storage. + * Data written to one instance must not be readable from another one. + */ + interface Factory { + + /** + * Creates an instance of a [PersistenceStrategy]. + * + * @param identifier the identifier for the persistence strategy. + * @param maxItemsPerBatch the maximum number of individual events in a batch + * @param maxBatchSize the maximum size (in bytes) of a complete batch + */ + fun create( + identifier: String, + maxItemsPerBatch: Int, + maxBatchSize: Long + ): PersistenceStrategy + } + + /** + * Describes the content of event batch. + * @property batchId the unique identifier for a batch + * @property metadata the metadata attached to the batch + * @property events the list of events in the batch + */ + data class Batch( + val batchId: String, + val metadata: ByteArray? = null, + val events: List = mutableListOf() + ) + + /** + * @return the metadata of the current writeable batch + */ + @WorkerThread + fun currentMetadata(): ByteArray? + + /** + * Writes the content of the event to the current available batch. + * @param event the event to write (content + metadata) + * @param batchMetadata the optional updated batch metadata + * @param eventType additional information about the event that can impact the way it is stored. Note: events + * with the CRASH type are being sent as part of the Crash Reporting feature, and implies that the process will + * exit soon, meaning that those event must be kept synchronously and in a way to be retrieved after the app + * restarts in a new process (e.g.: on the file system, or in a local database). + * + * @return true if event was written, false otherwise. + */ + @WorkerThread + fun write( + event: RawBatchEvent, + batchMetadata: ByteArray?, + eventType: EventType + ): Boolean + + /** + * Reads the next batch of data and lock it so that it can't be read or written to by anyone. + */ + @WorkerThread + fun lockAndReadNext(): Batch? + + /** + * Marks the batch as unlocked and to be kept to be read again later. + */ + @WorkerThread + fun unlockAndKeep(batchId: String) + + /** + * Marks the batch as unlocked and to be deleted. + * The corresponding batch should not be returned in any call to [lockAndReadNext]. + */ + @WorkerThread + fun unlockAndDelete(batchId: String) + + /** + * Drop all data. + */ + @WorkerThread + fun dropAll() + + /** + * Migrate the data to a different [PersistenceStrategy]. + * All readable and ongoing batches must be transferred to the given strategy. + */ + fun migrateData(targetStrategy: PersistenceStrategy) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/Serializer.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/Serializer.kt new file mode 100644 index 0000000000..756c00fe49 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/Serializer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.persistence + +import com.datadog.android.api.InternalLogger +import com.datadog.android.lint.InternalApi +import java.util.Locale + +/** + * An interface which can transform an object of type [T] into a formatted String. + */ +interface Serializer { + /** + * Serializes the data into a String. + * @return the String representing the data or null if any exception occurs + */ + fun serialize(model: T): String? + + companion object { + internal const val ERROR_SERIALIZING = "Error serializing %s model" + } +} + +/** + * A utility class to serialize a model to a ByteArray safely. + * If an exception is thrown while serializing the data, null is returned, and a + * message will be sent to the internalLogger. + * + * @param T Data type to serialize. + * @param model Data object to serialize. + * @param internalLogger Internal logger. + */ +@Suppress("TooGenericExceptionCaught") +@InternalApi +fun Serializer.serializeToByteArray( + model: T, + internalLogger: InternalLogger +): ByteArray? { + return try { + val serialized = serialize(model) + serialized?.toByteArray(Charsets.UTF_8) + } catch (e: Throwable) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + { Serializer.ERROR_SERIALIZING.format(Locale.US, model.javaClass.simpleName) }, + e + ) + null + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt new file mode 100644 index 0000000000..e6e8c2590e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.persistence.datastore + +/** + * Datastore entry contents and metadata. + * + * @param T type of data used by this entry in the datastore. + * @property versionCode version used by the entry. + * @property data content of the entry. + */ +data class DataStoreContent( + val versionCode: Int, + val data: T? +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/DeterministicSampler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/DeterministicSampler.kt new file mode 100644 index 0000000000..5256e6d160 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/DeterministicSampler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.sampling + +import androidx.annotation.FloatRange +import com.datadog.android.api.InternalLogger + +/** + * [Sampler] with the given sample rate using a deterministic algorithm for a stable + * sampling decision across sources. + * + * @param T the type of items to sample. + * @param idConverter a lambda converting the input item into a stable numerical identifier + * @param sampleRateProvider Provider for the sample rate value which will be called each time + * the sampling decision needs to be made. All the values should be on the scale [0;100]. + */ +open class DeterministicSampler( + private val idConverter: (T) -> ULong, + private val sampleRateProvider: () -> Float +) : Sampler { + + /** + * Creates a new instance lof [DeterministicSampler] with the given sample rate. + * + * @param idConverter a lambda converting the input item into a stable numerical identifier + * @param sampleRate Sample rate to use. + */ + constructor( + idConverter: (T) -> ULong, + @FloatRange(from = 0.0, to = 100.0) sampleRate: Float + ) : this(idConverter, { sampleRate }) + + /** + * Creates a new instance of [DeterministicSampler] with the given sample rate. + * + * @param idConverter a lambda converting the input item into a stable numerical identifier + * @param sampleRate Sample rate to use. + */ + constructor( + idConverter: (T) -> ULong, + @FloatRange(from = 0.0, to = 100.0) sampleRate: Double + ) : this(idConverter, sampleRate.toFloat()) + + /** @inheritDoc */ + override fun sample(item: T): Boolean { + val sampleRate = getSampleRate() + + return when { + sampleRate >= SAMPLE_ALL_RATE -> true + sampleRate <= 0f -> false + else -> { + val hash = idConverter(item) * SAMPLER_HASHER + val threshold = (MAX_ID.toDouble() * sampleRate / SAMPLE_ALL_RATE).toULong() + hash < threshold + } + } + } + + /** @inheritDoc */ + override fun getSampleRate(): Float { + val rawSampleRate = sampleRateProvider() + return if (rawSampleRate < 0f) { + InternalLogger.UNBOUND.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { "Sample rate value provided $rawSampleRate is below 0, setting it to 0." } + ) + 0f + } else if (rawSampleRate > SAMPLE_ALL_RATE) { + InternalLogger.UNBOUND.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { "Sample rate value provided $rawSampleRate is above 100, setting it to 100." } + ) + SAMPLE_ALL_RATE + } else { + rawSampleRate + } + } + + companion object { + /** + * Represents the maximum allowable sample rate in the sampling process. + */ + const val SAMPLE_ALL_RATE: Float = 100f + + /** + * A constant value used as a multiplier for generating deterministic hash values + * within the [DeterministicSampler] implementation. This value is a good number for + * Knuth hashing (large, prime, fit in 64 bit long). + */ + const val SAMPLER_HASHER: ULong = 1111111111111111111u + + /** + * The maximum value used as an upper limit for computing hash-based sampling thresholds. + */ + const val MAX_ID: ULong = 0xFFFFFFFFFFFFFFFFUL + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/RateBasedSampler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/RateBasedSampler.kt new file mode 100644 index 0000000000..ee49895879 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/RateBasedSampler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.sampling + +import androidx.annotation.FloatRange +import com.datadog.android.api.InternalLogger +import java.security.SecureRandom + +/** + * [Sampler] with the given sample rate which can be fixed or dynamic. + * + * @param T the type of items to sample. + * @param sampleRateProvider Provider for the sample rate value which will be called each time + * the sampling decision needs to be made. All the values should be on the scale [0;100]. + */ +open class RateBasedSampler(private val sampleRateProvider: () -> Float) : Sampler { + + /** + * Creates a new instance of [RateBasedSampler] with the given sample rate. + * + * @param sampleRate Sample rate to use. + */ + constructor(@FloatRange(from = 0.0, to = 100.0) sampleRate: Float) : this({ sampleRate }) + + /** + * Creates a new instance of [RateBasedSampler] with the given sample rate. + * + * @param sampleRate Sample rate to use. + */ + constructor(@FloatRange(from = 0.0, to = 100.0) sampleRate: Double) : this(sampleRate.toFloat()) + + private val random by lazy { SecureRandom() } + + /** @inheritDoc */ + @Suppress("MagicNumber") + override fun sample(item: T): Boolean { + return when (val sampleRate = getSampleRate()) { + 0f -> false + SAMPLE_ALL_RATE -> true + else -> random.nextFloat() * 100 <= sampleRate + } + } + + /** @inheritDoc */ + override fun getSampleRate(): Float { + val rawSampleRate = sampleRateProvider() + return if (rawSampleRate < 0f) { + InternalLogger.UNBOUND.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { "Sample rate value provided $rawSampleRate is below 0, setting it to 0." } + ) + 0f + } else if (rawSampleRate > SAMPLE_ALL_RATE) { + InternalLogger.UNBOUND.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { "Sample rate value provided $rawSampleRate is above 100, setting it to 100." } + ) + SAMPLE_ALL_RATE + } else { + rawSampleRate + } + } + + private companion object { + const val SAMPLE_ALL_RATE = 100f + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/Sampler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/Sampler.kt new file mode 100644 index 0000000000..189ff58332 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/sampling/Sampler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.sampling + +import androidx.annotation.FloatRange + +/** + * Interface representing the sampling. + * @param T the type of items to sample. + */ +interface Sampler { + + /** + * @param item the item to sample + * @return true to keep the item, false to discard it + */ + fun sample(item: T): Boolean + + /** + * @return the sample rate if applicable, as a float between 0 and 100, + * or null if not applicable + */ + @FloatRange(from = 0.0, to = 100.0) + fun getSampleRate(): Float? +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/thread/FlushableExecutorService.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/thread/FlushableExecutorService.kt new file mode 100644 index 0000000000..ce38ef0f05 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/thread/FlushableExecutorService.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.lint.InternalApi +import java.util.concurrent.ExecutorService + +/** + * An [ExecutorService] which backing queue can be drained to a collection. + */ +@InternalApi +interface FlushableExecutorService : ExecutorService { + + /** + * Drains the queue backing this [ExecutorService] into the provided mutable collection. + * After this operation, the executor's queue will be empty, and all the runnable entries added + * to the destination won't have run yet. + * + * @param destination the collection into which [Runnable] in the queue should be drained to. + */ + fun drainTo(destination: MutableCollection) + + /** + * A Factory for a [FlushableExecutorService] implementation. + */ + fun interface Factory { + + /** + * Create an instance of [FlushableExecutorService]. + * @param internalLogger the internal logger + * @param executorContext Context to be used for logging and naming threads running on this executor. + * @param backPressureStrategy the strategy to handle back-pressure + * @return the instance + */ + fun create( + internalLogger: InternalLogger, + executorContext: String, + backPressureStrategy: BackPressureStrategy + ): FlushableExecutorService + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/error/internal/CrashReportsFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/error/internal/CrashReportsFeature.kt new file mode 100644 index 0000000000..bf85cbb818 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/error/internal/CrashReportsFeature.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.error.internal + +import android.content.Context +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import java.util.concurrent.atomic.AtomicBoolean + +internal class CrashReportsFeature(private val sdkCore: FeatureSdkCore) : Feature { + + internal val initialized = AtomicBoolean(false) + internal var originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + + // region Feature + + override val name: String = CRASH_FEATURE_NAME + + override fun onInitialize(appContext: Context) { + setupExceptionHandler(appContext) + initialized.set(true) + } + + override fun onStop() { + resetOriginalExceptionHandler() + initialized.set(false) + } + + // endregion + + // region Internal + + private fun setupExceptionHandler( + appContext: Context + ) { + originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + DatadogExceptionHandler( + sdkCore = sdkCore, + appContext = appContext + ).register() + } + + private fun resetOriginalExceptionHandler() { + Thread.setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler) + } + + // endregion + + companion object { + internal const val CRASH_FEATURE_NAME = "crash" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/error/internal/DatadogExceptionHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/error/internal/DatadogExceptionHandler.kt new file mode 100644 index 0000000000..51eb001496 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/error/internal/DatadogExceptionHandler.kt @@ -0,0 +1,156 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.error.internal + +import android.content.Context +import androidx.work.WorkManager +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.feature.event.JvmCrash +import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.core.internal.thread.waitToIdle +import com.datadog.android.core.internal.utils.triggerUploadWorker +import com.datadog.android.internal.utils.asString +import com.datadog.android.internal.utils.loggableStackTrace +import java.lang.ref.WeakReference +import java.util.concurrent.ThreadPoolExecutor + +internal class DatadogExceptionHandler( + private val sdkCore: FeatureSdkCore, + appContext: Context +) : Thread.UncaughtExceptionHandler { + + private val contextRef = WeakReference(appContext) + private var previousHandler: Thread.UncaughtExceptionHandler? = null + + // region Thread.UncaughtExceptionHandler + + override fun uncaughtException(t: Thread, e: Throwable) { + val threads = getThreadDumps(t, e) + + // write a RUM Error too + val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + if (rumFeature != null) { + rumFeature.sendEvent( + JvmCrash.Rum( + throwable = e, + message = createCrashMessage(e), + threads = threads + ) + ) + } else { + sdkCore.internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + { MISSING_RUM_FEATURE_INFO } + ) + } + + // TODO RUM-3794 If DatadogExceptionHandler goes into dedicated module (module of 1 class + // only?), we have to wait for the write in some other way + // give some time to the persistence executor service to finish its tasks + if (sdkCore is InternalSdkCore) { + val idled = (sdkCore.getPersistenceExecutorService() as? ThreadPoolExecutor) + ?.waitToIdle(MAX_WAIT_FOR_IDLE_TIME_IN_MS, sdkCore.internalLogger) ?: true + if (!idled) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { EXECUTOR_NOT_IDLED_WARNING_MESSAGE } + ) + } + } + + // trigger a task to send the logs ASAP + contextRef.get()?.let { + if (WorkManager.isInitialized()) { + triggerUploadWorker(it, sdkCore.name, sdkCore.internalLogger) + } + } + + // Always do this one last; this will shut down the VM + previousHandler?.uncaughtException(t, e) + } + + // endregion + + // region DatadogExceptionHandler + + fun register() { + previousHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler(this) + } + + // endregion + + // region Internal + + private fun createCrashMessage(throwable: Throwable): String { + val rawMessage = throwable.message + return if (rawMessage.isNullOrBlank()) { + val className = throwable.javaClass.canonicalName ?: throwable.javaClass.simpleName + "$MESSAGE: $className" + } else { + rawMessage + } + } + + private fun getThreadDumps(crashedThread: Thread, e: Throwable): List { + return mutableListOf( + ThreadDump( + crashed = true, + name = crashedThread.name, + state = crashedThread.state.asString(), + stack = e.loggableStackTrace() + ) + ) + safeGetAllStacktraces() + .filterKeys { it != crashedThread } + .filterValues { it.isNotEmpty() } + .map { (thread, stackTrace) -> + ThreadDump( + name = thread.name, + state = thread.state.asString(), + stack = stackTrace.loggableStackTrace(), + crashed = false + ) + } + } + + private fun safeGetAllStacktraces(): Map> { + return try { + Thread.getAllStackTraces() + } catch (@Suppress("TooGenericExceptionCaught") t: Throwable) { + // coroutines machinery can throw errors here + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Failed to get all threads dump" }, + t + ) + emptyMap() + } + } + + // endregion + + companion object { + // If you change these you will have to propagate the changes + // also into the datadog-native-lib.cpp file inside the dd-sdk-android-ndk module. + internal const val LOGGER_NAME = CrashReportsFeature.CRASH_FEATURE_NAME + internal const val MESSAGE = "Application crash detected" + internal const val MAX_WAIT_FOR_IDLE_TIME_IN_MS = 100L + internal const val EXECUTOR_NOT_IDLED_WARNING_MESSAGE = + "Datadog SDK is in an unexpected state due to an ongoing crash. " + + "Some events could be lost." + internal const val MISSING_LOGS_FEATURE_INFO = + "Logs feature is not registered, won't report crash as log." + internal const val MISSING_RUM_FEATURE_INFO = + "RUM feature is not registered, won't report crash as RUM event." + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/EventMapper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/EventMapper.kt new file mode 100644 index 0000000000..94b0a685d8 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/EventMapper.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.event + +/** + * An interface which can be implemented to modify the writable attributes inside an event [T]. + */ +fun interface EventMapper { + /** + * By implementing this method you can intercept and modify the writable + * attributes inside any event [T] before it gets serialised. + * + * @param event the event to be serialised + * @return the modified event [T] or NULL + * + * Please note that if you return NULL from this method the event will be dropped and will not + * be serialised. If the object returned has a different reference than the object + * which was passed to the function, it will be dropped as well. + */ + fun map(event: T): T? +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/MapperSerializer.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/MapperSerializer.kt new file mode 100644 index 0000000000..386d182d50 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/MapperSerializer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.event + +import com.datadog.android.core.persistence.Serializer + +/** + * Combines [EventMapper] and [Serializer]. First mapping is done, then serialization. + * + * @param T type of the data to map and serialize. + * @param eventMapper Event mapper to use. + * @param serializer Serializer to use. + */ +class MapperSerializer( + private val eventMapper: EventMapper, + private val serializer: Serializer +) : Serializer { + + /** @inheritdoc */ + override fun serialize(model: T): String? { + val mappedEvent = eventMapper.map(model) ?: return null + return serializer.serialize(mappedEvent) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/NoOpEventMapper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/NoOpEventMapper.kt new file mode 100644 index 0000000000..9b0645e462 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/event/NoOpEventMapper.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.event + +/** + * No-op implementation of [EventMapper]. Will return the same instance. + * + * @param T type boundary of the mapped object. + */ +class NoOpEventMapper : EventMapper { + + /** @inheritdoc */ + override fun map(event: T): T { + return event + } + + /** @inheritdoc */ + override fun equals(other: Any?): Boolean { + return other is NoOpEventMapper<*> + } + + /** @inheritdoc */ + override fun hashCode(): Int { + return 0 + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/lint/InternalApi.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/lint/InternalApi.kt new file mode 100644 index 0000000000..1f8161eb9e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/lint/InternalApi.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.lint + +/** + * This annotation marks given method or property as internal, meaning it shouldn't be used + * outside of Datadog modules and it can be changed at any moment. + * + * This annotation participates in the lint check provided by the [InternalApiUsageDetector]. + * + * Note: Don't use this annotation on interfaces / non-final classes, because implementation will + * be flagged as internal as a whole, put it on individual methods/properties instead. + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) +annotation class InternalApi diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/LogAttributes.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/log/LogAttributes.kt similarity index 79% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/log/LogAttributes.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/log/LogAttributes.kt index ebf1c5cae4..af8d460c42 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/LogAttributes.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/log/LogAttributes.kt @@ -6,8 +6,12 @@ package com.datadog.android.log -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig +import com.datadog.android.api.SdkCore +import com.datadog.android.log.LogAttributes.ACCOUNT_ID +import com.datadog.android.log.LogAttributes.ACCOUNT_NAME +import com.datadog.android.log.LogAttributes.USR_EMAIL +import com.datadog.android.log.LogAttributes.USR_ID +import com.datadog.android.log.LogAttributes.USR_NAME /** * This class holds constant log attribute keys. @@ -105,6 +109,12 @@ object LogAttributes { */ const val ERROR_STACK: String = "error.stack" + /** + * The source type of the error. This value is used to indicate the language or platform + * that the error originates from, such as Flutter, React Native, or the NDK. (String) + */ + const val ERROR_SOURCE_TYPE: String = "error.source_type" + /** * The name of the originating host as defined in metrics. (String) * This value is automatically filled by the Datadog framework. @@ -156,7 +166,7 @@ object LogAttributes { /** * The name of the logger. (String) * This value is filled automatically by the [Logger]. - * @see [Logger.Builder.setLoggerName] + * @see [Logger.Builder.setName] */ const val LOGGER_NAME: String = "logger.name" @@ -255,11 +265,19 @@ object LogAttributes { */ const val RUM_VIEW_ID: String = "view.id" + /** + * The id of the active RUM Action. (String) + * This lets the RUM and Logs features to be linked. + * This value is filled automatically by the [Logger]. + * @see [Logger.Builder.bundleWithRumEnabled] + */ + const val RUM_ACTION_ID: String = "user_action.id" + /** * The name of the application or service generating the log events. (String) * This value is filled automatically by the [Logger]. - * @see [DatadogConfig.Builder.setServiceName] - * @see [Logger.Builder.setServiceName] + * @see [Configuration.Builder.service] + * @see [Logger.Builder.setService] */ const val SERVICE_NAME: String = "service" @@ -275,26 +293,72 @@ object LogAttributes { */ const val STATUS: String = "status" - internal const val USR_ATTRIBUTES_GROUP: String = "usr" + /** + * Group containing user properties. + * + * @see USR_EMAIL + * @see USR_ID + * @see USR_NAME + */ + const val USR_ATTRIBUTES_GROUP: String = "usr" /** * The user email. (String) * This value is filled automatically by the [Logger]. - * @see [Datadog.setUserInfo] + * @see [SdkCore.setUserInfo] */ const val USR_EMAIL: String = "$USR_ATTRIBUTES_GROUP.email" /** * The user identifier. (String) * This value is filled automatically by the [Logger]. - * @see [Datadog.setUserInfo] + * @see [SdkCore.setUserInfo] */ const val USR_ID: String = "$USR_ATTRIBUTES_GROUP.id" /** * The user friendly name. (String) * This value is filled automatically by the [Logger]. - * @see [Datadog.setUserInfo] + * @see [SdkCore.setUserInfo] */ const val USR_NAME: String = "$USR_ATTRIBUTES_GROUP.name" + + /** + * Group containing account properties. + * + * @see ACCOUNT_ID + * @see ACCOUNT_NAME + */ + const val ACCOUNT_ATTRIBUTES_GROUP: String = "account" + + /** + * The account identifier. (String) + * This value is filled automatically by the [Logger]. + * @see [SdkCore.setAccountInfo] + */ + const val ACCOUNT_ID: String = "$ACCOUNT_ATTRIBUTES_GROUP.id" + + /** + * The account friendly name. (String) + * This value is filled automatically by the [Logger]. + * @see [SdkCore.setAccountInfo] + */ + const val ACCOUNT_NAME: String = "$ACCOUNT_ATTRIBUTES_GROUP.name" + + /** + * The application variant. (String) + * This value is filled automatically by the [Logger]. + */ + const val VARIANT: String = "variant" + + /** + * The source type of an error. Used by cross platform tools to indicate the language + * or platform that the error originates from, such as Flutter or React Native (String). + */ + const val SOURCE_TYPE: String = "_dd.error.source_type" + + /** + * Specifies a custom error fingerprint for the supplied log. (String) + */ + const val ERROR_FINGERPRINT: String = "_dd.error.fingerprint" } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandler.kt new file mode 100644 index 0000000000..495069ed80 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.listFilesSafe +import com.datadog.android.core.internal.persistence.file.readTextSafe +import com.datadog.android.core.internal.utils.executeSafe +import com.google.gson.JsonObject +import java.io.File +import java.util.Locale +import java.util.concurrent.ExecutorService + +@Suppress("TooManyFunctions", "LongParameterList") +internal class DatadogNdkCrashHandler( + storageDir: File, + private val dataPersistenceExecutorService: ExecutorService, + private val ndkCrashLogDeserializer: Deserializer, + private val internalLogger: InternalLogger, + private val lastRumViewEventProvider: () -> JsonObject?, + internal val nativeCrashSourceType: String = "ndk" +) : NdkCrashHandler { + + internal val ndkCrashDataDirectory: File = getNdkGrantedDir(storageDir) + + internal var lastRumViewEvent: JsonObject? = null + internal var lastNdkCrashLog: NdkCrashLog? = null + + // region NdkCrashHandler + + override fun prepareData() { + dataPersistenceExecutorService.executeSafe("NDK crash check", internalLogger) { + readCrashData() + } + } + + override fun handleNdkCrash(sdkCore: FeatureSdkCore) { + dataPersistenceExecutorService.executeSafe("NDK crash report ", internalLogger) { + checkAndHandleNdkCrashReport(sdkCore) + } + } + + // endregion + + // region Internal + + @Suppress("NestedBlockDepth") + @WorkerThread + private fun readCrashData() { + if (!ndkCrashDataDirectory.existsSafe(internalLogger)) { + return + } + try { + lastRumViewEvent = lastRumViewEventProvider() + + ndkCrashDataDirectory.listFilesSafe(internalLogger)?.forEach { file -> + when (file.name) { + // TODO RUM-639 Data from NDK should be also encrypted + CRASH_DATA_FILE_NAME -> + lastNdkCrashLog = + file.readTextSafe(internalLogger = internalLogger)?.let { + ndkCrashLogDeserializer.deserialize(it) + } + } + } + } catch (e: SecurityException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { ERROR_READ_NDK_DIR }, + e + ) + } finally { + clearCrashLog() + } + } + + @WorkerThread + private fun checkAndHandleNdkCrashReport(sdkCore: FeatureSdkCore) { + if (lastNdkCrashLog != null) { + handleNdkCrashLog( + sdkCore, + lastNdkCrashLog, + lastRumViewEvent + ) + clearAllReferences() + } + } + + private fun clearAllReferences() { + lastRumViewEvent = null + lastNdkCrashLog = null + } + + @WorkerThread + private fun handleNdkCrashLog( + sdkCore: FeatureSdkCore, + ndkCrashLog: NdkCrashLog?, + lastViewEvent: JsonObject? + ) { + if (ndkCrashLog == null) { + return + } + val errorLogMessage = LOG_CRASH_MSG.format(Locale.US, ndkCrashLog.signalName) + + if (lastViewEvent != null) { + sendCrashRumEvent( + sdkCore, + errorLogMessage, + ndkCrashLog, + lastViewEvent + ) + } + } + + @Suppress("StringLiteralDuplication") + @WorkerThread + private fun sendCrashRumEvent( + sdkCore: FeatureSdkCore, + errorLogMessage: String, + ndkCrashLog: NdkCrashLog, + lastViewEvent: JsonObject + ) { + val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + if (rumFeature != null) { + rumFeature.sendEvent( + mapOf( + "type" to "ndk_crash", + "sourceType" to nativeCrashSourceType, + "timestamp" to ndkCrashLog.timestamp, + "timeSinceAppStartMs" to ndkCrashLog.timeSinceAppStartMs, + "signalName" to ndkCrashLog.signalName, + "stacktrace" to ndkCrashLog.stacktrace, + "message" to errorLogMessage, + "lastViewEvent" to lastViewEvent + ) + ) + } else { + internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + { INFO_RUM_FEATURE_NOT_REGISTERED } + ) + } + } + + @SuppressWarnings("TooGenericExceptionCaught") + private fun clearCrashLog() { + if (ndkCrashDataDirectory.existsSafe(internalLogger)) { + try { + ndkCrashDataDirectory.listFilesSafe(internalLogger) + ?.forEach { it.deleteRecursively() } + } catch (e: Throwable) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf( + InternalLogger.Target.MAINTAINER, + InternalLogger.Target.TELEMETRY + ), + { + "Unable to clear the NDK crash report file:" + + " ${ndkCrashDataDirectory.absolutePath}" + }, + e + ) + } + } + } + + // endregion + + companion object { + + private const val RUM_VIEW_EVENT_FILE_NAME = "last_view_event" + internal const val CRASH_DATA_FILE_NAME = "crash_log" + internal const val USER_INFO_FILE_NAME = "user_information" + internal const val NETWORK_INFO_FILE_NAME = "network_information" + + internal const val LOG_CRASH_MSG = "NDK crash detected with signal: %s" + internal const val ERROR_READ_NDK_DIR = "Error while trying to read the NDK crash directory" + + internal const val INFO_RUM_FEATURE_NOT_REGISTERED = + "RUM feature is not registered, won't report NDK crash info as RUM error." + + private const val STORAGE_VERSION = 2 + + internal const val NDK_CRASH_REPORTS_FOLDER_NAME = "ndk_crash_reports_v$STORAGE_VERSION" + private const val NDK_CRASH_REPORTS_PENDING_FOLDER_NAME = + "ndk_crash_reports_intermediary_v$STORAGE_VERSION" + + private fun getNdkGrantedDir(storageDir: File): File { + return File(storageDir, NDK_CRASH_REPORTS_FOLDER_NAME) + } + + private fun getNdkPendingDir(storageDir: File): File { + return File(storageDir, NDK_CRASH_REPORTS_PENDING_FOLDER_NAME) + } + + @Deprecated( + "We will still process this path to check file from the old SDK" + + " versions, but don't use it anymore for writing." + ) + internal fun getLastViewEventFile(storageDir: File): File { + return File(getNdkGrantedDir(storageDir), RUM_VIEW_EVENT_FILE_NAME) + } + + internal fun getPendingNetworkInfoFile(storageDir: File): File { + return File(getNdkPendingDir(storageDir), NETWORK_INFO_FILE_NAME) + } + + internal fun getGrantedNetworkInfoFile(storageDir: File): File { + return File(getNdkGrantedDir(storageDir), NETWORK_INFO_FILE_NAME) + } + + internal fun getPendingUserInfoFile(storageDir: File): File { + return File(getNdkPendingDir(storageDir), USER_INFO_FILE_NAME) + } + + internal fun getGrantedUserInfoFile(storageDir: File): File { + return File(getNdkGrantedDir(storageDir), USER_INFO_FILE_NAME) + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashHandler.kt new file mode 100644 index 0000000000..1e41158768 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface NdkCrashHandler { + + fun prepareData() + + fun handleNdkCrash(sdkCore: FeatureSdkCore) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashLog.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashLog.kt new file mode 100644 index 0000000000..c70eb89ad4 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashLog.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.lang.IllegalStateException + +internal data class NdkCrashLog( + val signal: Int, + val timestamp: Long, + val timeSinceAppStartMs: Long?, + val signalName: String, + val message: String, + val stacktrace: String +) { + + internal fun toJson(): String { + val jsonObject = JsonObject() + jsonObject.addProperty(SIGNAL_KEY_NAME, signal) + jsonObject.addProperty(SIGNAL_NAME_KEY_NAME, signalName) + jsonObject.addProperty(TIMESTAMP_KEY_NAME, timestamp) + jsonObject.addProperty(TIME_SINCE_APP_START_MS_NAME, timeSinceAppStartMs) + jsonObject.addProperty(MESSAGE_KEY_NAME, message) + jsonObject.addProperty(STACKTRACE_KEY_NAME, stacktrace) + return jsonObject.toString() + } + + companion object { + + internal const val SIGNAL_KEY_NAME = "signal" + internal const val TIMESTAMP_KEY_NAME = "timestamp" + internal const val TIME_SINCE_APP_START_MS_NAME = "time_since_app_start_ms" + internal const val MESSAGE_KEY_NAME = "message" + internal const val SIGNAL_NAME_KEY_NAME = "signal_name" + internal const val STACKTRACE_KEY_NAME = "stacktrace" + + @Suppress("UnsafeThirdPartyFunctionCall") + @Throws(JsonParseException::class, IllegalStateException::class) + internal fun fromJson(jsonString: String): NdkCrashLog { + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + return NdkCrashLog( + jsonObject.get(SIGNAL_KEY_NAME).asInt, + jsonObject.get(TIMESTAMP_KEY_NAME).asLong, + jsonObject.get(TIME_SINCE_APP_START_MS_NAME) + ?.let { if (it is JsonNull) null else it.asLong }, + jsonObject.get(SIGNAL_NAME_KEY_NAME).asString, + jsonObject.get(MESSAGE_KEY_NAME).asString, + jsonObject.get(STACKTRACE_KEY_NAME).asString + ) + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashLogDeserializer.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashLogDeserializer.kt new file mode 100644 index 0000000000..ed56008775 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashLogDeserializer.kt @@ -0,0 +1,44 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.Deserializer +import com.google.gson.JsonParseException +import java.util.Locale + +internal class NdkCrashLogDeserializer( + private val internalLogger: InternalLogger +) : Deserializer { + + override fun deserialize(model: String): NdkCrashLog? { + return try { + NdkCrashLog.fromJson(model) + } catch (e: JsonParseException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { DESERIALIZE_ERROR_MESSAGE_FORMAT.format(Locale.US, model) }, + e + ) + null + } catch (e: IllegalStateException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { DESERIALIZE_ERROR_MESSAGE_FORMAT.format(Locale.US, model) }, + e + ) + null + } + } + + companion object { + const val DESERIALIZE_ERROR_MESSAGE_FORMAT = + "Error while trying to deserialize the NDK Crash info: %s" + } +} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/privacy/TrackingConsent.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/privacy/TrackingConsent.kt similarity index 100% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/privacy/TrackingConsent.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/privacy/TrackingConsent.kt diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/privacy/TrackingConsentProviderCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/privacy/TrackingConsentProviderCallback.kt similarity index 100% rename from dd-sdk-android/src/main/kotlin/com/datadog/android/privacy/TrackingConsentProviderCallback.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/privacy/TrackingConsentProviderCallback.kt diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/security/Encryption.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/security/Encryption.kt new file mode 100644 index 0000000000..3054fc4dd0 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/security/Encryption.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.security + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * 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). + */ +@NoOpImplementation +interface Encryption { + /** + * Encrypts given [ByteArray] with user-chosen encryption. + * @param data Bytes to encrypt. + */ + fun encrypt(data: ByteArray): ByteArray + + /** + * Decrypts given [ByteArray] with user-chosen encryption. + * @param data Bytes to decrypt. 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). + */ + fun decrypt(data: ByteArray): ByteArray +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/trace/TracingHeaderType.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/trace/TracingHeaderType.kt new file mode 100644 index 0000000000..82df536c86 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/trace/TracingHeaderType.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.trace + +/** + * Defines the list of tracing header types that can be injected into http requests. + * @property headerType Explicit header type property introduced in order to have a consistent value + * in case if enum values are renamed. + */ +enum class TracingHeaderType(val headerType: String) { + /** + * 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). + */ + DATADOG("DATADOG"), + + /** + * Open Telemetry B3 [Single header](https://github.com/openzipkin/b3-propagation#single-header). + */ + B3("B3"), + + /** + * Open Telemetry B3 [Multiple headers](https://github.com/openzipkin/b3-propagation#multiple-headers). + */ + B3MULTI("B3MULTI"), + + /** + * W3C [Trace Context header](https://www.w3.org/TR/trace-context/#tracestate-header). + */ + TRACECONTEXT("TRACECONTEXT") +} diff --git a/dd-sdk-android-core/src/test/java/com/datadog/trace/sampling/JavaDeterministicSampler.java b/dd-sdk-android-core/src/test/java/com/datadog/trace/sampling/JavaDeterministicSampler.java new file mode 100644 index 0000000000..cb9917c536 --- /dev/null +++ b/dd-sdk-android-core/src/test/java/com/datadog/trace/sampling/JavaDeterministicSampler.java @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.trace.sampling; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.datadog.android.core.sampling.Sampler; + +/** + * This is a pseudo-duplicate of the java implementation for testing purposes only to ensure + * compatibility between our generic implementation and the one in our backend agent. + */ +public class JavaDeterministicSampler implements Sampler { + + private static final long KNUTH_FACTOR = 1111111111111111111L; + + private static final double MAX = Math.pow(2, 64) - 1; + + private final float rate; + + public JavaDeterministicSampler(float rate) { + this.rate = rate; + } + + @Override + public boolean sample(@NonNull Long item) { + return item * KNUTH_FACTOR + Long.MIN_VALUE < cutoff(rate); + } + + @Nullable + @Override + public Float getSampleRate() { + return rate; + } + + private long cutoff(double rate) { + if (rate < 0.5) { + return (long) (rate * MAX) + Long.MIN_VALUE; + } + if (rate < 1.0) { + return (long) ((rate * MAX) + Long.MIN_VALUE); + } + return Long.MAX_VALUE; + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/DatadogSiteTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/DatadogSiteTest.kt new file mode 100644 index 0000000000..7b58e40b83 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/DatadogSiteTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogSiteTest { + + @Test + fun `M return intake endpoint W intakeEndpoint {US1}`() { + assertThat(DatadogSite.US1.intakeEndpoint).isEqualTo("https://browser-intake-datadoghq.com/") + } + + @Test + fun `M return intake endpoint W intakeEndpoint {US3}`() { + assertThat(DatadogSite.US3.intakeEndpoint).isEqualTo("https://browser-intake-us3-datadoghq.com/") + } + + @Test + fun `M return intake endpoint W intakeEndpoint {US5}`() { + assertThat(DatadogSite.US5.intakeEndpoint).isEqualTo("https://browser-intake-us5-datadoghq.com/") + } + + @Test + fun `M return intake endpoint W intakeEndpoint {US1-FED}`() { + assertThat(DatadogSite.US1_FED.intakeEndpoint).isEqualTo("https://browser-intake-ddog-gov.com/") + } + + @Test + fun `M return intake endpoint W intakeEndpoint {EU1}`() { + assertThat(DatadogSite.EU1.intakeEndpoint).isEqualTo("https://browser-intake-datadoghq.eu/") + } + + @Test + fun `M return intake endpoint W intakeEndpoint {AP1}`() { + assertThat(DatadogSite.AP1.intakeEndpoint).isEqualTo("https://browser-intake-ap1-datadoghq.com/") + } + + @Test + fun `M return intake endpoint W intakeEndpoint {AP2}`() { + assertThat(DatadogSite.AP2.intakeEndpoint).isEqualTo("https://browser-intake-ap2-datadoghq.com/") + } + + @Test + fun `M return intake endpoint W intakeEndpoint {STAGING}`() { + assertThat(DatadogSite.STAGING.intakeEndpoint).isEqualTo("https://browser-intake-datad0g.com/") + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/DatadogTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/DatadogTest.kt new file mode 100644 index 0000000000..5906ddc82f --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/DatadogTest.kt @@ -0,0 +1,632 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android + +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.internal.CoreFeature +import com.datadog.android.core.internal.DatadogCore +import com.datadog.android.core.internal.HashGenerator +import com.datadog.android.core.internal.NoOpInternalSdkCore +import com.datadog.android.core.internal.SdkCoreRegistry +import com.datadog.android.core.internal.Sha256HashGenerator +import com.datadog.android.internal.utils.loggableStackTrace +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.config.InternalLoggerTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.annotations.ProhibitLeavingStaticMocksIn +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.ProhibitLeavingStaticMocksExtension +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ProhibitLeavingStaticMocksExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +@ProhibitLeavingStaticMocksIn(Datadog::class) +internal class DatadogTest { + + @Mock + lateinit var mockConnectivityMgr: ConnectivityManager + + @Forgery + lateinit var fakeConfiguration: Configuration + + @Forgery + lateinit var fakeConsent: TrackingConsent + + @BeforeEach + fun `set up`() { + whenever(appContext.mockInstance.getSystemService(Context.CONNECTIVITY_SERVICE)) + .doReturn(mockConnectivityMgr) + + CoreFeature.disableKronosBackgroundSync = true + } + + @AfterEach + fun `tear down`() { + Datadog.hashGenerator = Sha256HashGenerator() + Datadog.stopInstance() + Datadog.registry.clear() + } + + // region initialize + + @Test + fun `M return sdk instance W initialize() + getInstance()`() { + // When + val initialized = Datadog.initialize( + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + val instance = Datadog.getInstance() + + // Then + assertThat(instance).isSameAs(initialized) + } + + @Test + fun `M return sdk instance W initialize(name) + getInstance(name)`( + @StringForgery name: String + ) { + // When + val initialized = Datadog.initialize( + name, + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + val instance = Datadog.getInstance(name) + + // Then + assertThat(instance).isSameAs(initialized) + } + + @Test + fun `M warn W initialize() + initialize()`() { + // When + val initialized1 = Datadog.initialize( + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + val initialized2 = Datadog.initialize( + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // Then + logger.mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + Datadog.MESSAGE_ALREADY_INITIALIZED + ) + assertThat(initialized2).isSameAs(initialized1) + } + + @Test + fun `M warn W initialize(name) + initialize(name)`( + @StringForgery name: String + ) { + // When + Datadog.initialize(name, appContext.mockInstance, fakeConfiguration, fakeConsent) + Datadog.initialize(name, appContext.mockInstance, fakeConfiguration, fakeConsent) + + // Then + logger.mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + Datadog.MESSAGE_ALREADY_INITIALIZED + ) + } + + @Test + fun `M create instance ID W initialize()`( + @Forgery fakeConfiguration: Configuration, + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) fakeHash: String + ) { + // Given + val mockHashGenerator: HashGenerator = mock() + whenever( + mockHashGenerator.generate( + "null/${fakeConfiguration.coreConfig.site.siteName}" + ) + ) doReturn fakeHash + Datadog.hashGenerator = mockHashGenerator + + // When + val instance = Datadog.initialize( + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // Then + check(instance is DatadogCore) + assertThat(instance.instanceId).isEqualTo(fakeHash) + } + + @Test + fun `M create instance ID W initialize(name)`( + @StringForgery instanceName: String, + @Forgery fakeConfiguration: Configuration, + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) fakeHash: String + ) { + // Given + val mockHashGenerator: HashGenerator = mock() + whenever( + mockHashGenerator.generate( + "$instanceName/${fakeConfiguration.coreConfig.site.siteName}" + ) + ) doReturn fakeHash + Datadog.hashGenerator = mockHashGenerator + + // When + val instance = Datadog.initialize( + instanceName, + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // Then + check(instance is DatadogCore) + assertThat(instance.instanceId).isEqualTo(fakeHash) + } + + @Test + fun `M set tracking consent W initialize()`( + @Forgery fakeConfiguration: Configuration, + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) fakeHash: String + ) { + // Given + val mockHashGenerator: HashGenerator = mock() + whenever( + mockHashGenerator.generate( + "null/${fakeConfiguration.coreConfig.site.siteName}" + ) + ) doReturn fakeHash + Datadog.hashGenerator = mockHashGenerator + + // When + val instance = Datadog.initialize( + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // Then + check(instance is DatadogCore) + assertThat(instance.trackingConsent).isEqualTo(fakeConsent) + } + + @Test + fun `M set tracking consent W initialize(name)`( + @StringForgery instanceName: String, + @Forgery fakeConfiguration: Configuration, + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) fakeHash: String + ) { + // Given + val mockHashGenerator: HashGenerator = mock() + whenever( + mockHashGenerator.generate( + "$instanceName/${fakeConfiguration.coreConfig.site.siteName}" + ) + ) doReturn fakeHash + Datadog.hashGenerator = mockHashGenerator + + // When + val instance = Datadog.initialize( + instanceName, + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // Then + check(instance is DatadogCore) + assertThat(instance.trackingConsent).isEqualTo(fakeConsent) + } + + @Test + fun `M warn W initialize() {hash generator fails}`() { + // Given + Datadog.hashGenerator = mock() + whenever(Datadog.hashGenerator.generate(any())) doReturn null + + // When + val instance = Datadog.initialize( + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // Then + logger.mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + Datadog.CANNOT_CREATE_SDK_INSTANCE_ID_ERROR + ) + assertThat(instance).isNull() + } + + @Test + fun `M warn W initialize(name) {hash generator fails}`( + @StringForgery name: String + ) { + // Given + Datadog.hashGenerator = mock() + whenever(Datadog.hashGenerator.generate(any())) doReturn null + + // When + val instance = Datadog.initialize( + name, + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // Then + logger.mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + Datadog.CANNOT_CREATE_SDK_INSTANCE_ID_ERROR + ) + assertThat(instance).isNull() + } + + @Test + fun `M stop specific instance W stopInstance()`() { + // Given + val sdk = Datadog.initialize( + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) as? DatadogCore + checkNotNull(sdk) + + // When + Datadog.stopInstance() + val getInstance = Datadog.getInstance() + + // Then + assertThat(getInstance).isInstanceOf(NoOpInternalSdkCore::class.java) + assertThat(sdk.coreFeature.initialized.get()).isFalse() + } + + @Test + fun `M stop specific instance W stopInstance(name)`( + @StringForgery name: String + ) { + // Given + val sdk = Datadog.initialize( + name, + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) as? DatadogCore + checkNotNull(sdk) + + // When + Datadog.stopInstance(name) + val getInstance = Datadog.getInstance(name) + + // Then + assertThat(getInstance).isInstanceOf(NoOpInternalSdkCore::class.java) + assertThat(sdk.coreFeature.initialized.get()).isFalse() + } + + @Test + fun `M not stop specific instance W stopInstance(name) {different name}`( + @StringForgery name: String, + @StringForgery name2: String + ) { + // Given + val sdk = Datadog.initialize( + name, + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) as? DatadogCore + checkNotNull(sdk) + + // When + Datadog.stopInstance(name2) + val getInstance = Datadog.getInstance(name) + + // Then + assertThat(getInstance).isSameAs(sdk) + assertThat(sdk.coreFeature.initialized.get()).isTrue() + } + + @Test + fun `M warn W getInstance() { instance is not initialized }`( + forge: Forge + ) { + // Given + val fakeInstanceName = forge.aNullable { anAlphabeticalString() } + + // When + Datadog.getInstance(fakeInstanceName) + + // Then + val currentMethodName = Thread.currentThread().stackTrace[1].methodName + val expectedStacktrace = Throwable().fillInStackTrace() + .loggableStackTrace() + .lines() + .drop(1) + .filter { !it.contains(currentMethodName) } + .joinToString(separator = "\n") + argumentCaptor<() -> String> { + verify(logger.mockInternalLogger).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + val actualMessage = firstValue() + val filteredActualMessage = actualMessage + .lines() + // need to filter out, because we cannot reproduce exactly the same stacktrace + .filter { !it.contains(currentMethodName) && !it.contains("getInstance") } + .joinToString(separator = "\n") + assertThat(filteredActualMessage) + .isEqualTo( + Datadog.MESSAGE_SDK_NOT_INITIALIZED.format( + Locale.US, + fakeInstanceName ?: SdkCoreRegistry.DEFAULT_INSTANCE_NAME, + expectedStacktrace + ) + ) + } + } + + @Test + fun `M return false W isInitialized() { instance is not initialized }`( + forge: Forge + ) { + // Given + val fakeInstanceName = forge.aNullable { anAlphabeticalString() } + + // When + val result = Datadog.isInitialized(fakeInstanceName) + + // Then + assertThat(result).isFalse + verifyNoInteractions(logger.mockInternalLogger) + } + + @Test + fun `M return true W isInitialized() { instance is initialized }`( + forge: Forge + ) { + // Given + val fakeInstanceName = forge.aNullable { anAlphabeticalString() } + + Datadog.initialize( + fakeInstanceName, + appContext.mockInstance, + fakeConfiguration, + fakeConsent + ) + + // When + val result = Datadog.isInitialized(fakeInstanceName) + + // Then + assertThat(result).isTrue() + verifyNoInteractions(logger.mockInternalLogger) + } + + // endregion + + @Test + fun `M set and get lib verbosity W setVerbosity() + getVerbosity()`( + @IntForgery level: Int + ) { + // When + Datadog.setVerbosity(level) + val result = Datadog.getVerbosity() + + // Then + assertThat(result).isEqualTo(level) + } + + @Test + fun `M do nothing W stop() without initialize`() { + // When + Datadog.stopInstance() + + // Then + verifyNoInteractions(appContext.mockInstance) + } + + @Test + fun `M set tracking consent W setTrackingConsent()`( + @Forgery fakeTrackingConsent: TrackingConsent + ) { + // Given + val mockSdkCore = mock() + + // When + Datadog.setTrackingConsent(fakeTrackingConsent, mockSdkCore) + + // Then + verify(mockSdkCore).setTrackingConsent(fakeTrackingConsent) + } + + @Test + fun `M set user info W setUserInfo()`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + val mockSdkCore = mock() + + // When + Datadog.setUserInfo(id, name, email, fakeUserProperties, mockSdkCore) + + // Then + verify(mockSdkCore).setUserInfo(id, name, email, fakeUserProperties) + } + + @Test + fun `M add user properties W addUserProperties()`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + val mockSdkCore = mock() + + // When + Datadog.addUserProperties(fakeUserProperties, mockSdkCore) + + // Then + verify(mockSdkCore).addUserProperties(fakeUserProperties) + } + + @Test + fun `M clear user info W clearUserInfo()`() { + // Given + val mockSdkCore = mock() + + // When + Datadog.clearUserInfo(mockSdkCore) + + // Then + verify(mockSdkCore).clearUserInfo() + } + + @Test + fun `M clear all data W clearAllData()`() { + // Given + val mockSdkCore = mock() + + // When + Datadog.clearAllData(mockSdkCore) + + // Then + verify(mockSdkCore).clearAllData() + } + + @Test + fun `M call Core set account info W setAccountInfo()`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeExtraInfo: Map + ) { + // Given + val mockSdkCore = mock() + + // When + Datadog.setAccountInfo( + id = id, + name = name, + extraInfo = fakeExtraInfo, + sdkCore = mockSdkCore + ) + + // Then + verify(mockSdkCore).setAccountInfo(id, name, fakeExtraInfo) + } + + @Test + fun `M call Core add account extra info W addAccountExtraInfo()`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeExtraInfo: Map + ) { + // Given + val mockSdkCore = mock() + + // When + Datadog.addAccountExtraInfo( + extraInfo = fakeExtraInfo, + sdkCore = mockSdkCore + ) + + // Then + verify(mockSdkCore).addAccountExtraInfo(fakeExtraInfo) + } + + @Test + fun `M call Core clear account info W clearAccountInfo()`() { + // Given + val mockSdkCore = mock() + + // When + Datadog.clearAccountInfo(sdkCore = mockSdkCore) + + // Then + verify(mockSdkCore).clearAccountInfo() + } + companion object { + val appContext = ApplicationContextTestConfiguration(Application::class.java) + val logger = InternalLoggerTestConfiguration() + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(logger, appContext) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/InternalProxyTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/InternalProxyTest.kt new file mode 100644 index 0000000000..e98669ffb8 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/InternalProxyTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android + +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.core.internal.CoreFeature +import com.datadog.android.core.internal.DatadogCore +import com.datadog.android.core.internal.system.AppVersionProvider +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class InternalProxyTest { + + @Test + fun `M proxy telemetry to RumMonitor W debug()`( + @StringForgery message: String + ) { + // Given + val mockSdkCore = mock() + val mockRumFeatureScope = mock() + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope + val proxy = _InternalProxy(mockSdkCore) + + // When + proxy._telemetry.debug(message) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Debug + assertThat(logEvent.message).isEqualTo(message) + } + } + + @Test + fun `M proxy telemetry to RumMonitor W error()`( + @StringForgery message: String, + @StringForgery stack: String, + @StringForgery kind: String + ) { + // Given + val mockSdkCore = mock() + val mockRumFeatureScope = mock() + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope + val proxy = _InternalProxy(mockSdkCore) + + // When + proxy._telemetry.error(message, stack, kind) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Error + assertThat(logEvent.message).isEqualTo(message) + assertThat(logEvent.stacktrace).isEqualTo(stack) + assertThat(logEvent.kind).isEqualTo(kind) + } + } + + @Test + fun `M proxy telemetry to RumMonitor W error({message, throwable})`( + @StringForgery message: String, + @Forgery throwable: Throwable + ) { + // Given + val mockSdkCore = mock() + val mockRumFeatureScope = mock() + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope + val proxy = _InternalProxy(mockSdkCore) + + // When + proxy._telemetry.error(message, throwable) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Error + assertThat(logEvent.message).isEqualTo(message) + assertThat(logEvent.error).isEqualTo(throwable) + } + } + + @Test + fun `M set app version W setCustomAppVersion()`( + @StringForgery version: String + ) { + // Given + val mockSdkCore = mock() + val mockAppVersionProvider = mock() + val mockCoreFeature = mock() + whenever(mockCoreFeature.packageVersionProvider) doReturn mockAppVersionProvider + whenever(mockSdkCore.coreFeature) doReturn mockCoreFeature + val proxy = _InternalProxy(mockSdkCore) + + // When + proxy.setCustomAppVersion(version) + + // Then + verify(mockAppVersionProvider).version = version + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/api/context/NetworkInfoTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/api/context/NetworkInfoTest.kt new file mode 100644 index 0000000000..2ec90a5cd9 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/api/context/NetworkInfoTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings +@ForgeConfiguration(Configurator::class) +internal class NetworkInfoTest { + + @RepeatedTest(8) + fun `M serialize deserialized event W toJson()+fromJson()`( + @Forgery event: NetworkInfo + ) { + // Given + val json = event.toJson().toString() + + // When + val result = NetworkInfo.fromJson(json) + + // Then + Assertions.assertThat(result).isEqualTo(result) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/api/context/UserInfoTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/api/context/UserInfoTest.kt new file mode 100644 index 0000000000..ceca761b37 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/api/context/UserInfoTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.context + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings +@ForgeConfiguration(Configurator::class) +internal class UserInfoTest { + + @RepeatedTest(8) + fun `M serialize deserialized event W toJson()+fromJson()`( + @Forgery event: UserInfo + ) { + // Given + val json = event.toJson().toString() + + // When + val result = UserInfo.fromJson(json) + + // Then + Assertions.assertThat(result).isEqualTo(result) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreInitializationTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreInitializationTest.kt new file mode 100644 index 0000000000..3e922b5cfc --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreInitializationTest.kt @@ -0,0 +1,637 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core + +import android.app.Application +import android.content.pm.ApplicationInfo +import android.util.Log +import com.datadog.android.Datadog +import com.datadog.android.api.feature.Feature +import com.datadog.android.core.configuration.BatchProcessingLevel +import com.datadog.android.core.configuration.BatchSize +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.configuration.UploadFrequency +import com.datadog.android.core.internal.CoreFeature +import com.datadog.android.core.internal.DatadogContextProvider +import com.datadog.android.core.internal.DatadogCore +import com.datadog.android.core.internal.SdkFeature +import com.datadog.android.core.thread.FlushableExecutorService +import com.datadog.android.error.internal.CrashReportsFeature +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.security.Encryption +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.forge.CustomAttributes +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.net.URL + +/** + * This region groups all test about instantiating a DatadogCore instance. + * Note: eventually most of the work done upon initialization will be moved + * somewhere else + */ +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogCoreInitializationTest { + + lateinit var testedCore: DatadogCore + + @Mock + lateinit var mockPersistenceExecutorService: FlushableExecutorService + + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) + lateinit var fakeInstanceId: String + + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) + lateinit var fakeInstanceName: String + + @Forgery + lateinit var fakeConfiguration: Configuration + + @BeforeEach + fun `set up`() { + CoreFeature.disableKronosBackgroundSync = true + + whenever(mockPersistenceExecutorService.execute(any())) doAnswer { + it.getArgument(0).run() + } + } + + @AfterEach + fun `tear down`() { + if (this::testedCore.isInitialized) { + testedCore.stop() + } + } + + @RepeatedTest(4) + fun `M initialize requested features W initialize()`( + @BoolForgery crashReportsEnabled: Boolean + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize(fakeConfiguration.copy(crashReportsEnabled = crashReportsEnabled)) + } + + // Then + assertThat(testedCore.coreFeature.initialized.get()).isTrue + assertThat(testedCore.isActive).isTrue + assertThat(testedCore.contextProvider).isInstanceOf(DatadogContextProvider::class.java) + + assertThat(testedCore.getFeature(CrashReportsFeature.CRASH_FEATURE_NAME)).let { + if (crashReportsEnabled) { + it.isNotNull + } else { + it.isNull() + } + } + } + + @Test + fun `M throw an error W initialize() {envName not valid, isDebug=false}`( + @IntForgery fakeFlags: Int, + @StringForgery(regex = "[\\$%\\*@][a-zA-Z0-9_:./-]{0,200}") invalidEnvName: String + ) { + // Given + appContext.fakeAppInfo.flags = fakeFlags and ApplicationInfo.FLAG_DEBUGGABLE.inv() + + // When + val exception = assertThrows { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName + ).apply { + initialize(fakeConfiguration.copy(env = invalidEnvName)) + } + } + + // Then + assertThat(exception) + .hasMessage(DatadogCore.MESSAGE_ENV_NAME_NOT_VALID) + } + + @Test + fun `M throw an error W initialize() {envName not valid, isDebug=true}`( + @IntForgery fakeFlags: Int, + @StringForgery(regex = "[\\$%\\*@][a-zA-Z0-9_:./-]{0,200}") invalidEnvName: String + ) { + // Given + appContext.fakeAppInfo.flags = fakeFlags or ApplicationInfo.FLAG_DEBUGGABLE + + // When + val exception = assertThrows { + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName + ) + .apply { + initialize(fakeConfiguration.copy(env = invalidEnvName)) + } + } + + // Then + assertThat(exception) + .hasMessage(DatadogCore.MESSAGE_ENV_NAME_NOT_VALID) + } + + @Test + fun `M initialize the ConsentProvider with PENDING W initializing()`() { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize(fakeConfiguration) + } + + // Then + assertThat(testedCore.coreFeature.trackingConsentProvider.getConsent()) + .isEqualTo(TrackingConsent.PENDING) + } + + @Test + fun `M not set lib verbosity W initializing() {dev mode when debug, debug=false}`( + @IntForgery fakeFlags: Int + ) { + // Given + Datadog.setVerbosity(Int.MAX_VALUE) + appContext.fakeAppInfo.flags = fakeFlags and ApplicationInfo.FLAG_DEBUGGABLE.inv() + + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + coreConfig = fakeConfiguration.coreConfig.copy( + enableDeveloperModeWhenDebuggable = true + ) + ) + ) + } + + // Then + assertThat(Datadog.libraryVerbosity) + .isEqualTo(Int.MAX_VALUE) + } + + @Test + fun `M set lib verbosity W initializing() {dev mode when debug, debug=true}`( + @IntForgery fakeFlags: Int + ) { + // Given + appContext.fakeAppInfo.flags = fakeFlags or ApplicationInfo.FLAG_DEBUGGABLE + + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + coreConfig = fakeConfiguration.coreConfig.copy( + enableDeveloperModeWhenDebuggable = true + ) + ) + ) + } + + // Then + assertThat(Datadog.libraryVerbosity) + .isEqualTo(Log.VERBOSE) + } + + @Test + fun `M not set isDeveloperModeEnabled W initializing() {dev mode when debug, debug=false}`( + @IntForgery fakeFlags: Int + ) { + // Given + Datadog.setVerbosity(Int.MAX_VALUE) + appContext.fakeAppInfo.flags = fakeFlags and ApplicationInfo.FLAG_DEBUGGABLE.inv() + + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + coreConfig = fakeConfiguration.coreConfig.copy( + enableDeveloperModeWhenDebuggable = true + ) + ) + ) + } + + // Then + assertThat(testedCore.isDeveloperModeEnabled) + .isFalse + } + + @Test + fun `M set isDeveloperModeEnabled W initializing() {dev mode when debug, debug=true}`( + @IntForgery fakeFlags: Int + ) { + // Given + appContext.fakeAppInfo.flags = fakeFlags or ApplicationInfo.FLAG_DEBUGGABLE + + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + coreConfig = fakeConfiguration.coreConfig.copy( + enableDeveloperModeWhenDebuggable = true + ) + ) + ) + } + + // Then + assertThat(testedCore.isDeveloperModeEnabled) + .isTrue + } + + @Test + fun `M submit core config telemetry W initializing()`( + forge: Forge + ) { + // Given + val trackErrors = forge.aBool() + val useProxy = forge.aBool() + val useLocalEncryption = forge.aBool() + val usePersistenceStrategyFactory = forge.aBool() + val batchSize = forge.aValueFrom(BatchSize::class.java) + val uploadFrequency = forge.aValueFrom(UploadFrequency::class.java) + val batchProcessingLevel = forge.aValueFrom(BatchProcessingLevel::class.java) + + val configuration = Configuration.Builder( + clientToken = fakeConfiguration.clientToken, + env = fakeConfiguration.env, + variant = fakeConfiguration.variant, + service = fakeConfiguration.service + ).apply { + if (useProxy) { + setProxy(mock(), forge.aNullable { mock() }) + } + if (useLocalEncryption) { + val mockEncryption = mock() + whenever(mockEncryption.encrypt(any())) doAnswer { it.getArgument(0) } + whenever(mockEncryption.decrypt(any())) doAnswer { it.getArgument(0) } + setEncryption(mockEncryption) + } + if (usePersistenceStrategyFactory) { + setPersistenceStrategyFactory(mock()) + } + } + .setBatchSize(batchSize) + .setUploadFrequency(uploadFrequency) + .setBatchProcessingLevel(batchProcessingLevel) + .setCrashReportsEnabled(trackErrors) + .build() + + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize(configuration) + } + + // Then + val mockRumFeature = mock() + testedCore.features += Feature.RUM_FEATURE_NAME to mockRumFeature + + val mockTracingFeature = mock() + testedCore.features += Feature.TRACING_FEATURE_NAME to mockTracingFeature + + testedCore.coreFeature.uploadExecutorService.queue + .toTypedArray() + .forEach { + it.run() + } + testedCore.coreFeature.uploadExecutorService.shutdownNow() + + argumentCaptor { + verify(mockRumFeature).sendEvent(capture()) + val telemetryConfigurationEvent = firstValue as InternalTelemetryEvent.Configuration + assertThat(telemetryConfigurationEvent.trackErrors) + .isEqualTo(configuration.crashReportsEnabled) + assertThat(telemetryConfigurationEvent.batchSize) + .isEqualTo(configuration.coreConfig.batchSize.windowDurationMs) + assertThat(telemetryConfigurationEvent.useLocalEncryption) + .isEqualTo(configuration.coreConfig.encryption != null) + assertThat(telemetryConfigurationEvent.batchUploadFrequency) + .isEqualTo(configuration.coreConfig.uploadFrequency.baseStepMs) + assertThat(telemetryConfigurationEvent.batchProcessingLevel) + .isEqualTo(configuration.coreConfig.batchProcessingLevel.maxBatchesPerUploadJob) + assertThat(telemetryConfigurationEvent.useProxy) + .isEqualTo(configuration.coreConfig.proxy != null) + } + } + + // region AdditionalConfig + + @Test + fun `M apply source name W applyAdditionalConfig(config) { with source name }`( + @StringForgery(type = StringForgeryType.ALPHABETICAL) source: String + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize(fakeConfiguration.copy(additionalConfig = mapOf(Datadog.DD_SOURCE_TAG to source))) + } + + // Then + assertThat(testedCore.coreFeature.sourceName).isEqualTo(source) + } + + @Test + fun `M use default source name W applyAdditionalConfig(config) { with empty source name }`( + @StringForgery(type = StringForgeryType.WHITESPACE) source: String + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy(additionalConfig = mapOf(Datadog.DD_SOURCE_TAG to source)) + ) + } + + // Then + assertThat(testedCore.coreFeature.sourceName).isEqualTo(CoreFeature.DEFAULT_SOURCE_NAME) + } + + @Test + fun `M use default source name W applyAdditionalConfig(config) { with source name !string }`( + @IntForgery source: Int + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy(additionalConfig = mapOf(Datadog.DD_SOURCE_TAG to source)) + ) + } + + // Then + assertThat(testedCore.coreFeature.sourceName).isEqualTo(CoreFeature.DEFAULT_SOURCE_NAME) + } + + @Test + fun `M use default source name W applyAdditionalConfig(config) { without source name }`( + @Forgery customAttributes: CustomAttributes + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize(fakeConfiguration.copy(additionalConfig = customAttributes.nonNullData)) + } + + // Then + assertThat(testedCore.coreFeature.sourceName).isEqualTo(CoreFeature.DEFAULT_SOURCE_NAME) + } + + @Test + fun `M apply sdk version W applyAdditionalConfig(config) { with sdk version }`( + @StringForgery(regex = "[0-9]+(\\.[0-9]+)+") sdkVersion: String + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + additionalConfig = mapOf(Datadog.DD_SDK_VERSION_TAG to sdkVersion) + ) + ) + } + + // Then + assertThat(testedCore.coreFeature.sdkVersion).isEqualTo(sdkVersion) + } + + @Test + fun `M use default sdk version W applyAdditionalConfig(config) { with empty sdk version }`( + @StringForgery(type = StringForgeryType.WHITESPACE) sdkVersion: String + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + additionalConfig = mapOf(Datadog.DD_SDK_VERSION_TAG to sdkVersion) + ) + ) + } + + // Then + assertThat(testedCore.coreFeature.sdkVersion).isEqualTo(CoreFeature.DEFAULT_SDK_VERSION) + } + + @Test + fun `M use default sdk version W applyAdditionalConfig(config) { with sdk version !string }`( + @Forgery sdkVersion: URL + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + additionalConfig = mapOf(Datadog.DD_SDK_VERSION_TAG to sdkVersion) + ) + ) + } + + // Then + assertThat(testedCore.coreFeature.sdkVersion).isEqualTo(CoreFeature.DEFAULT_SDK_VERSION) + } + + @Test + fun `M use default sdk version W applyAdditionalConfig(config) { without sdk version }`( + @Forgery customAttributes: CustomAttributes + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize(fakeConfiguration.copy(additionalConfig = customAttributes.nonNullData)) + } + + // Then + assertThat(testedCore.coreFeature.sdkVersion).isEqualTo(CoreFeature.DEFAULT_SDK_VERSION) + } + + @Test + fun `M apply app version W applyAdditionalConfig(config) { with app version }`( + @StringForgery appVersion: String + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + additionalConfig = mapOf(Datadog.DD_APP_VERSION_TAG to appVersion) + ) + ) + } + + // Then + assertThat(testedCore.coreFeature.packageVersionProvider.version).isEqualTo(appVersion) + } + + @Test + fun `M use default app version W applyAdditionalConfig(config) { with empty app version }`( + forge: Forge + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + additionalConfig = mapOf(Datadog.DD_APP_VERSION_TAG to forge.aWhitespaceString()) + ) + ) + } + + // Then + assertThat(testedCore.coreFeature.packageVersionProvider.version).isEqualTo( + appContext.fakeVersionName + ) + } + + @Test + fun `M use default app version W applyAdditionalConfig(config) { with app version !string }`( + forge: Forge + ) { + // When + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService } + ).apply { + initialize( + fakeConfiguration.copy( + additionalConfig = mapOf(Datadog.DD_APP_VERSION_TAG to forge.anInt()) + ) + ) + } + + // Then + assertThat(testedCore.coreFeature.packageVersionProvider.version).isEqualTo( + appContext.fakeVersionName + ) + } + + // endregion + + companion object { + val appContext = ApplicationContextTestConfiguration(Application::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt new file mode 100644 index 0000000000..fdfcd4480a --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt @@ -0,0 +1,1489 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core + +import android.app.Application +import android.os.Build +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureContextUpdateReceiver +import com.datadog.android.api.feature.FeatureEventReceiver +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.internal.CoreFeature +import com.datadog.android.core.internal.DatadogCore +import com.datadog.android.core.internal.NoOpContextProvider +import com.datadog.android.core.internal.SdkFeature +import com.datadog.android.core.internal.account.MutableAccountInfoProvider +import com.datadog.android.core.internal.lifecycle.ProcessLifecycleCallback +import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor +import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver +import com.datadog.android.core.internal.net.info.NetworkInfoProvider +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.internal.system.BuildSdkVersionProvider +import com.datadog.android.core.internal.user.MutableUserInfoProvider +import com.datadog.android.core.thread.FlushableExecutorService +import com.datadog.android.internal.time.DefaultTimeProvider +import com.datadog.android.internal.time.TimeProvider +import com.datadog.android.ndk.internal.NdkCrashHandler +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.datadog.tools.unit.forge.aThrowable +import com.datadog.tools.unit.forge.exhaustiveAttributes +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.MapForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.AssertionFailureBuilder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Collections +import java.util.Locale +import java.util.Random +import java.util.UUID +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock + +/** + * This region groups all test about DatadogCore instance (except Initialization). + */ +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(value = Configurator::class) +internal class DatadogCoreTest { + + private lateinit var testedCore: DatadogCore + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockPersistenceExecutorService: FlushableExecutorService + + @Mock + lateinit var mockContextExecutorService: ThreadPoolExecutor + + @Mock + lateinit var mockBuildSdkVersionProvider: BuildSdkVersionProvider + + @Forgery + lateinit var fakeConfiguration: Configuration + + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) + lateinit var fakeInstanceId: String + + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) + lateinit var fakeInstanceName: String + + @BeforeEach + fun `set up`() { + CoreFeature.disableKronosBackgroundSync = true + whenever(mockPersistenceExecutorService.execute(any())) doAnswer { + it.getArgument(0).run() + } + whenever(mockContextExecutorService.execute(any())) doAnswer { + it.getArgument(0).run() + } + whenever(mockContextExecutorService.submit(any>())) doAnswer { + val result = it.getArgument>(0).call() + mock>().apply { whenever(get()) doReturn result } + } + + testedCore = DatadogCore( + appContext.mockInstance, + fakeInstanceId, + fakeInstanceName, + internalLoggerProvider = { mockInternalLogger }, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService }, + buildSdkVersionProvider = mockBuildSdkVersionProvider + ).apply { + initialize(fakeConfiguration) + } + testedCore.coreFeature.contextExecutorService = mockContextExecutorService + } + + @AfterEach + fun `tear down`() { + testedCore.stop() + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class) + fun `M update the ConsentProvider W setConsent`(fakeConsent: TrackingConsent) { + // Given + val mockConsentProvider = mock() + testedCore.coreFeature.trackingConsentProvider = mockConsentProvider + + // When + testedCore.setTrackingConsent(fakeConsent) + + // Then + verify(mockConsentProvider).setConsent(fakeConsent) + } + + @Test + fun `M register feature W registerFeature()`( + @Mock mockFeature: Feature, + @StringForgery fakeFeatureName: String + ) { + // Given + whenever(mockFeature.name) doReturn fakeFeatureName + + // When + testedCore.registerFeature(mockFeature) + + // Then + assertThat(testedCore.features).containsKey(fakeFeatureName) + verify(mockFeature).onInitialize(appContext.mockInstance) + } + + @Test + fun `M handle NDK crash for RUM W registerFeature() {RUM feature}`( + @Mock mockFeature: Feature + ) { + // Given + val mockNdkCrashHandler = mock() + testedCore.coreFeature.ndkCrashHandler = mockNdkCrashHandler + whenever(mockFeature.name) doReturn Feature.RUM_FEATURE_NAME + + // When + testedCore.registerFeature(mockFeature) + + // Then + verify(testedCore.coreFeature.ndkCrashHandler).handleNdkCrash(testedCore) + } + + @Test + fun `M do nothing W registerFeature() {Logs feature}`( + @Mock mockFeature: Feature + ) { + // Given + val mockNdkCrashHandler = mock() + testedCore.coreFeature.ndkCrashHandler = mockNdkCrashHandler + whenever(mockFeature.name) doReturn Feature.LOGS_FEATURE_NAME + + // When + testedCore.registerFeature(mockFeature) + + // Then + verifyNoInteractions(testedCore.coreFeature.ndkCrashHandler) + } + + @Test + fun `M update userInfoProvider W setUserInfo()`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + val mockUserInfoProvider = mock() + testedCore.coreFeature.userInfoProvider = mockUserInfoProvider + + // When + testedCore.setUserInfo(id, name, email, fakeUserProperties) + + // Then + verify(mockUserInfoProvider).setUserInfo(id, name, email, fakeUserProperties) + } + + @Test + fun `M update anonymousId W setAnonymousId()`( + forge: Forge + ) { + // Given + val uuid = forge.getForgery() + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean(true)) + val mockUserInfoProvider = mock() + whenever(testedCore.coreFeature.userInfoProvider) doReturn mockUserInfoProvider + whenever(testedCore.coreFeature.contextExecutorService) doReturn mockContextExecutorService + + // When + testedCore.setAnonymousId(uuid) + + // Then + verify(mockUserInfoProvider).setAnonymousId(uuid.toString()) + } + + @Test + fun `M clear anonymousId W setAnonymousId(null)`() { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean(true)) + val mockUserInfoProvider = mock() + whenever(testedCore.coreFeature.userInfoProvider) doReturn mockUserInfoProvider + whenever(testedCore.coreFeature.contextExecutorService) doReturn mockContextExecutorService + + // When + testedCore.setAnonymousId(null) + + // Then + verify(mockUserInfoProvider).setAnonymousId(null) + } + + @Test + fun `M set additional user info W addUserProperties() is called`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean(true)) + val mockUserInfoProvider = mock() + whenever(testedCore.coreFeature.userInfoProvider) doReturn mockUserInfoProvider + whenever(testedCore.coreFeature.contextExecutorService) doReturn mockContextExecutorService + + // When + testedCore.setUserInfo(id, name, email) + testedCore.addUserProperties( + mapOf( + "key1" to 1, + "key2" to "one" + ) + ) + + // Then + verify(mockUserInfoProvider).setUserInfo( + id, + name, + email, + emptyMap() + ) + verify(mockUserInfoProvider).addUserProperties( + properties = mapOf( + "key1" to 1, + "key2" to "one" + ) + ) + } + + @Test + fun `M clear user info W clearUserInfo()`() { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean(true)) + val mockUserInfoProvider = mock() + whenever(testedCore.coreFeature.userInfoProvider) doReturn mockUserInfoProvider + whenever(testedCore.coreFeature.contextExecutorService) doReturn mockContextExecutorService + + // When + testedCore.clearUserInfo() + + // Then + verify(mockUserInfoProvider).clearUserInfo() + } + + @Test + fun `M update accountInfoProvider W setAccountInfo()`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeAccountProperties: Map + ) { + // Given + val mockAccountInfoProvider = mock() + testedCore.coreFeature.accountInfoProvider = mockAccountInfoProvider + + // When + testedCore.setAccountInfo(id, name, fakeAccountProperties) + + // Then + verify(mockAccountInfoProvider).setAccountInfo(id, name, fakeAccountProperties) + } + + @Test + fun `M set additional account info W addAccountExtraInfo() is called`( + forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String + ) { + // Given + val fakeExtraProperties = forge.exhaustiveAttributes(excludedKeys = setOf("id", "name")) + val mockAccountInfoProvider = mock() + testedCore.coreFeature.accountInfoProvider = mockAccountInfoProvider + + // When + testedCore.setAccountInfo(id, name, emptyMap()) + testedCore.addAccountExtraInfo(fakeExtraProperties) + + // Then + verify(mockAccountInfoProvider).setAccountInfo( + id, + name, + emptyMap() + ) + verify(mockAccountInfoProvider).addExtraInfo(extraInfo = fakeExtraProperties) + } + + @Test + fun `M clear account info W clearAccountInfo()`() { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean(true)) + val mockAccountInfoProvider = mock() + whenever(testedCore.coreFeature.accountInfoProvider) doReturn mockAccountInfoProvider + whenever(testedCore.coreFeature.contextExecutorService) doReturn mockContextExecutorService + + // When + testedCore.clearAccountInfo() + + // Then + verify(mockAccountInfoProvider).clearAccountInfo() + } + + // region update + read feature context + + @Test + fun `M update feature context W updateFeatureContext()`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContext: Map, + @BoolForgery fakeUseContextThread: Boolean + ) { + // Given + val mockFeature = mock().apply { + whenever(featureContextLock) doReturn ReentrantReadWriteLock() + whenever(featureContext) doReturn mutableMapOf() + } + testedCore.features[feature] = mockFeature + val mockFeatureContextUpdateListener = mock() + testedCore.setContextUpdateReceiver(mockFeatureContextUpdateListener) + + // When + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.putAll(fakeContext) + } + + // Then + assertThat(mockFeature.featureContext).isEqualTo(fakeContext) + verify(mockFeatureContextUpdateListener).onContextUpdate(feature, fakeContext) + } + + @Test + fun `M update feature context W updateFeatureContext() { concurrent access }`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContextA: Map, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContextB: Map, + @BoolForgery fakeUseContextThread: Boolean, + forge: Forge + ) { + // Given + val mockFeature = mock() + whenever(mockFeature.featureContextLock) doReturn ReentrantReadWriteLock() + whenever(mockFeature.featureContext) doReturn mutableMapOf() + val otherFeatures = mapOf( + forge.anAlphaNumericalString() to mock() + ) + testedCore.features[feature] = mockFeature + testedCore.features.putAll(otherFeatures) + + // When + listOf( + Thread { + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.clear() + it.putAll(fakeContextA) + } + }, + Thread { + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.clear() + it.putAll(fakeContextB) + } + } + ).shuffled(Random(forge.seed)) + .map { it.apply { start() } } + .forEach { it.join() } + + // Then + // should be either of the two, but never something partial + assertThat(mockFeature.featureContext).isIn(fakeContextA, fakeContextB) + } + + @Timeout(5, unit = TimeUnit.SECONDS) + @Test + fun `M update feature context W updateFeatureContext() { no deadlock }`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContextA: Map, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContextB: Map, + @BoolForgery fakeUseContextThread: Boolean, + forge: Forge + ) { + // Given + val mockFeature = mock() + whenever(mockFeature.featureContextLock) doReturn ReentrantReadWriteLock() + whenever(mockFeature.featureContext) doReturn mutableMapOf() + val otherFeatures = mapOf( + forge.anAlphaNumericalString() to mock() + ) + testedCore.features[feature] = mockFeature + testedCore.features.putAll(otherFeatures) + + // When + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + Thread { + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.putAll(fakeContextA) + } + }.apply { start() } + .join() + it.putAll(fakeContextB) + } + + // Then + assertThat(mockFeature.featureContext).isEqualTo(fakeContextB) + } + + @Test + fun `M allow reentrant update W updateFeatureContext()`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContextA: Map, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContextB: Map, + @BoolForgery fakeUseContextThread: Boolean, + forge: Forge + ) { + // Given + val mockFeature = mock() + whenever(mockFeature.featureContextLock) doReturn ReentrantReadWriteLock() + whenever(mockFeature.featureContext) doReturn mutableMapOf() + val otherFeatures = mapOf( + forge.anAlphaNumericalString() to mock() + ) + testedCore.features[feature] = mockFeature + testedCore.features.putAll(otherFeatures) + + // When + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.putAll(fakeContextA) + } + it.putAll(fakeContextB) + } + + // Then + assertThat(mockFeature.featureContext).isEqualTo(fakeContextA + fakeContextB) + } + + @Test + fun `M do nothing W updateFeatureContext() { feature is not registered }`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContext: Map, + @BoolForgery fakeUseContextThread: Boolean + ) { + // When + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.putAll(fakeContext) + } + + // Then + testedCore.features.forEach { + assertThat(it.value.featureContext).doesNotContain(*fakeContext.entries.toTypedArray()) + } + } + + @Test + fun `M do nothing W updateFeatureContext() { write lock wasn't acquired }`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContext: Map, + @BoolForgery fakeUseContextThread: Boolean + ) { + // Given + val mockFeature = mock() + val mockRwLock = mock() + whenever(mockFeature.featureContextLock) doReturn mockRwLock + val mockWriteLock = mock() + whenever(mockRwLock.writeLock()) doReturn mockWriteLock + whenever(mockWriteLock.tryLock(any(), any())) doReturn false + whenever(mockFeature.featureContext) doReturn mutableMapOf() + testedCore.features[feature] = mockFeature + + // When + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.putAll(fakeContext) + } + + // Then + assertThat(mockFeature.featureContext).isEmpty() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Couldn't acquire ${mockWriteLock::class.java.simpleName} due to" + + " timeout (1 ${TimeUnit.SECONDS}), aborting operation." + ) + } + + @Test + fun `M read feature context W getFeatureContext()`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContext: Map, + @BoolForgery fakeUseContextThread: Boolean + ) { + // Given + val mockFeature = mock() + whenever(mockFeature.featureContextLock) doReturn ReentrantReadWriteLock() + whenever(mockFeature.featureContext) doReturn fakeContext.toMutableMap() + testedCore.features[feature] = mockFeature + + // When + val actualContext = testedCore.getFeatureContext(feature, fakeUseContextThread) + + // Then + assertThat(actualContext).isEqualTo(fakeContext) + } + + @Test + fun `M return immutable feature context W getFeatureContext() {feature context is changed after context creation}`( + @StringForgery feature: String, + @BoolForgery fakeUseContextThread: Boolean, + forge: Forge + ) { + // Given + val mockFeature = mock() + val mutableContext = forge.exhaustiveAttributes().toMutableMap() + val initialContext = mutableContext.toMap() + whenever(mockFeature.featureContextLock) doReturn ReentrantReadWriteLock() + whenever(mockFeature.featureContext) doReturn mutableContext + val keysToRemove = mutableContext.keys.take(forge.anInt(min = 1, max = mutableContext.keys.size)) + testedCore.features[feature] = mockFeature + + // When + val actualContext = testedCore.getFeatureContext(feature, fakeUseContextThread) + keysToRemove.forEach { + mutableContext.remove(it) + } + + // Then + assertThat(actualContext).isEqualTo(initialContext) + assertThat(actualContext).isNotEqualTo(mutableContext) + } + + @Test + fun `M read updated feature context W getFeatureContext() { read when update is in progress }`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeContext: Map, + @BoolForgery fakeUseContextThread: Boolean + ) { + // Given + val mockFeature = mock() + whenever(mockFeature.featureContextLock) doReturn ReentrantReadWriteLock() + whenever(mockFeature.featureContext) doReturn mutableMapOf() + testedCore.features[feature] = mockFeature + val countDownLatch = CountDownLatch(1) + + // When + val workerThread = Thread { + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + countDownLatch.countDown() + Thread.sleep(100) + it.putAll(fakeContext) + } + }.apply { start() } + countDownLatch.await(1, TimeUnit.SECONDS) + val actualContext = testedCore.getFeatureContext(feature, fakeUseContextThread) + workerThread.join(TimeUnit.SECONDS.toMillis(1)) + + // Then + assertThat(actualContext).isEqualTo(fakeContext) + } + + @Test + fun `M read initial feature context W getFeatureContext() { update when read is in progress }`( + @StringForgery feature: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeInitialContext: Map, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) fakeNewContext: Map, + @BoolForgery fakeUseContextThread: Boolean + ) { + // Given + val mockFeature = mock() + val rwLock = ReentrantReadWriteLock() + val updateStartLatch = CountDownLatch(1) + val mockRwLock = mock().apply { + whenever(readLock()) doAnswer { + mock().apply { + whenever(lock()) doAnswer { + rwLock.readLock().lock() + updateStartLatch.countDown() + } + whenever(unlock()) doAnswer { + rwLock.readLock().unlock() + } + } + } + whenever(writeLock()) doReturn rwLock.writeLock() + } + whenever(mockFeature.featureContextLock) doReturn mockRwLock + whenever(mockFeature.featureContext) doReturn fakeInitialContext.toMutableMap() + testedCore.features[feature] = mockFeature + + // When + val workerThread = Thread { + updateStartLatch.await(1, TimeUnit.SECONDS) + testedCore.updateFeatureContext(feature, fakeUseContextThread) { + it.putAll(fakeNewContext) + } + }.apply { start() } + val actualContext = testedCore.getFeatureContext(feature, fakeUseContextThread) + workerThread.join(TimeUnit.SECONDS.toMillis(1)) + + // Then + assertThat(actualContext).isEqualTo(fakeInitialContext) + } + + // endregion + + @Test + fun `M set event receiver W setEventReceiver()`( + @StringForgery feature: String + ) { + // Given + val mockFeature = mock() + val mockEventReceiverRef = mock>() + whenever(mockFeature.eventReceiver) doReturn mockEventReceiverRef + testedCore.features[feature] = mockFeature + + val fakeReceiver = FeatureEventReceiver { } + + // When + testedCore.setEventReceiver(feature, fakeReceiver) + + // Then + verify(mockEventReceiverRef).set(fakeReceiver) + } + + @Test + fun `M notify no feature registered W setEventReceiver() { feature is not registered }`( + @StringForgery feature: String + ) { + // Given + val fakeReceiver = FeatureEventReceiver { } + reset(mockInternalLogger) + + // When + testedCore.setEventReceiver(feature, fakeReceiver) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogCore.MISSING_FEATURE_FOR_EVENT_RECEIVER.format(Locale.US, feature) + ) + } + + @Test + fun `M notify receiver exists W setEventReceiver() { feature already has receiver }`( + @StringForgery feature: String + ) { + // Given + val mockFeature = mock() + val mockEventReceiverRef = mock>() + whenever(mockFeature.eventReceiver) doReturn mockEventReceiverRef + whenever(mockEventReceiverRef.get()) doReturn mock() + testedCore.features[feature] = mockFeature + val fakeReceiver = FeatureEventReceiver { } + reset(mockInternalLogger) + + // When + testedCore.setEventReceiver(feature, fakeReceiver) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogCore.EVENT_RECEIVER_ALREADY_EXISTS.format(Locale.US, feature) + ) + } + + @Test + fun `M remove receiver W removeEventReceiver()`( + @StringForgery feature: String + ) { + // Given + val mockFeature = mock() + val mockEventReceiverRef = mock>() + whenever(mockFeature.eventReceiver) doReturn mockEventReceiverRef + whenever(mockEventReceiverRef.get()) doReturn mock() + testedCore.features[feature] = mockFeature + + // When + testedCore.removeEventReceiver(feature) + + // Then + verify(mockEventReceiverRef).set(null) + } + + @Test + fun `M set context update listener W setContextUpdateReceiver()`() { + // Given + val mockContextUpdateListener = mock() + + // When + testedCore.setContextUpdateReceiver(mockContextUpdateListener) + + // Then + assertThat(testedCore.featureContextUpdateReceivers).contains(mockContextUpdateListener) + } + + @Test + fun `M notify listener already registered W setContextUpdateReceiver()`() { + // Given + val mockContextUpdateListener = mock() + + // When + testedCore.setContextUpdateReceiver(mockContextUpdateListener) + testedCore.setContextUpdateReceiver(mockContextUpdateListener) + + // Then + assertThat(testedCore.featureContextUpdateReceivers).contains(mockContextUpdateListener) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogCore.CONTEXT_UPDATE_LISTENER_ALREADY_REGISTERED.format(Locale.US, mockContextUpdateListener) + ) + } + + @Test + fun `M not throw W concurrent access to featureContextUpdateReceivers`( + forge: Forge + ) { + // Given + val mockListeners = forge.aList(size = forge.anInt(min = 2, max = 10)) { + mock() + } + val removeListeners = mockListeners.take(forge.anInt(min = 1, max = mockListeners.size)) + val fakeContext = forge.aMap { forge.anAlphabeticalString() to forge.anAlphabeticalString() } + val fakeFeatureName = forge.anAlphabeticalString() + val iterationRepeats = forge.anInt(min = 1, max = 10) + + // When + Then + mockListeners.map { + Thread { + assertDoesNotThrow { + testedCore.setContextUpdateReceiver(it) + } + }.apply { start() } + }.forEach { it.join(5000) } + val removeHandles = removeListeners.map { + Thread { + assertDoesNotThrow { + testedCore.removeContextUpdateReceiver(it) + } + }.apply { start() } + } + val iterationHandles = (0..iterationRepeats).map { + Thread { + assertDoesNotThrow { + testedCore.featureContextUpdateReceivers.forEach { + it.onContextUpdate(fakeFeatureName, fakeContext) + } + } + }.apply { start() } + } + + (removeHandles + iterationHandles).forEach { it.join() } + } + + @Test + fun `M not invoke listener W setContextUpdateReceiver() { feature has no context yet }`( + @StringForgery feature: String + ) { + // Given + val mockFeature = mock().apply { + whenever(featureContext) doReturn mutableMapOf() + whenever(featureContextLock) doReturn ReentrantReadWriteLock() + } + val mockContextUpdateListener = mock() + testedCore.features[feature] = mockFeature + + // When + testedCore.setContextUpdateReceiver(mockContextUpdateListener) + + // Then + verifyNoInteractions(mockContextUpdateListener) + } + + @Test + fun `M invoke listener W setContextUpdateReceiver() { feature has context }`( + @StringForgery feature: String, + forge: Forge + ) { + // Given + val fakeContext = forge.exhaustiveAttributes() + val mockFeature = mock().apply { + whenever(featureContext) doReturn fakeContext + whenever(featureContextLock) doReturn ReentrantReadWriteLock() + } + val mockContextUpdateListener = mock() + testedCore.features[feature] = mockFeature + + // When + testedCore.setContextUpdateReceiver(mockContextUpdateListener) + + // Then + verify(mockContextUpdateListener).onContextUpdate(feature, fakeContext) + } + + @Test + fun `M invoke listener W setContextUpdateReceiver() { feature update is in progress }`( + @StringForgery feature: String, + forge: Forge + ) { + // Given + val rwLock = ReentrantReadWriteLock() + val countDownLatch = CountDownLatch(1) + val mockFeature = mock().apply { + whenever(featureContext) doReturn mutableMapOf() + } + val mockRwLock = mock() + whenever(mockRwLock.writeLock()) doReturn rwLock.writeLock() + whenever(mockRwLock.readLock()) doAnswer { + countDownLatch.await(1, TimeUnit.SECONDS) + rwLock.readLock() + } + whenever(mockFeature.featureContextLock) doReturn mockRwLock + + testedCore.features[feature] = mockFeature + val fakeNewContext = forge.exhaustiveAttributes() + val mockContextUpdateListener = mock() + + // When + Thread { + testedCore.updateFeatureContext(feature) { + countDownLatch.countDown() + it.putAll(fakeNewContext) + } + }.apply { start() } + testedCore.setContextUpdateReceiver(mockContextUpdateListener) + + // Then + verify(mockContextUpdateListener).onContextUpdate(feature, fakeNewContext) + } + + @Test + fun `M remove context update listener W removeContextUpdateReceiver()`() { + // Given + val mockContextUpdateListener: FeatureContextUpdateReceiver = mock() + testedCore.featureContextUpdateReceivers += mockContextUpdateListener + + // When + testedCore.removeContextUpdateReceiver(mockContextUpdateListener) + + // Then + assertThat(testedCore.featureContextUpdateReceivers).isEmpty() + } + + @Test + fun `M provide name W name(){}`() { + // When+Then + assertThat(testedCore.name).isEqualTo(fakeInstanceName) + } + + @Test + fun `M provide time info W time()`( + @LongForgery(min = 10001L) fakeDeviceTimestamp: Long, + @LongForgery(min = -10000L, max = 10000L) fakeServerTimeOffsetMs: Long + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean()) + val mockTimeProvider = mock() + whenever(testedCore.coreFeature.timeProvider) doReturn mockTimeProvider + whenever(mockTimeProvider.getServerOffsetNanos()) doReturn TimeUnit.MILLISECONDS.toNanos( + fakeServerTimeOffsetMs + ) + whenever(mockTimeProvider.getServerOffsetMillis()) doReturn fakeServerTimeOffsetMs + whenever(mockTimeProvider.getDeviceTimestamp()) doReturn fakeDeviceTimestamp + whenever( + mockTimeProvider.getServerTimestamp() + ) doReturn fakeDeviceTimestamp + fakeServerTimeOffsetMs + + // When + val time = testedCore.time + + // Then + assertThat(time).isEqualTo( + TimeInfo( + deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(fakeDeviceTimestamp), + serverTimeNs = TimeUnit.MILLISECONDS.toNanos( + fakeDeviceTimestamp + fakeServerTimeOffsetMs + ), + serverTimeOffsetMs = fakeServerTimeOffsetMs, + serverTimeOffsetNs = TimeUnit.MILLISECONDS.toNanos(fakeServerTimeOffsetMs) + ) + ) + } + + @Test + fun `M provide time info without correction W time() {NoOpTimeProvider}`() { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean()) + whenever(testedCore.coreFeature.timeProvider) doReturn DefaultTimeProvider() + + // When + val time = testedCore.time + + // Then + // We do keep a margin of 1ms delay as this test can sometimes be flaky. + // the DatadogCore.time implementation computes server and device time independently and it can sometimes + // happen that those computations land on successive ms, leading to a 1second offset + assertThat(time.deviceTimeNs).isCloseTo(time.serverTimeNs, Offset.offset(msToNs)) + assertThat(time.serverTimeOffsetMs).isLessThanOrEqualTo(1) + assertThat(time.serverTimeOffsetNs).isLessThanOrEqualTo(msToNs) + } + + @Test + fun `M provide service W service()`( + @StringForgery fakeService: String + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.serviceName) doReturn fakeService + + // When + val service = testedCore.service + + // Then + assertThat(service).isEqualTo(fakeService) + } + + @Test + fun `M provide first party host resolver W firstPartyHostResolver()`() { + // Given + testedCore.coreFeature = mock() + val mockResolver = mock() + whenever(testedCore.coreFeature.firstPartyHostHeaderTypeResolver) doReturn mockResolver + + // When + val resolver = testedCore.firstPartyHostResolver + + // Then + assertThat(resolver).isSameAs(mockResolver) + } + + @Test + fun `M provide network info W networkInfo()`( + @Forgery fakeNetworkInfo: NetworkInfo + ) { + // Given + testedCore.coreFeature = mock() + val mockNetworkInfoProvider = mock() + whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo + whenever(testedCore.coreFeature.networkInfoProvider) doReturn mockNetworkInfoProvider + + // When + val networkInfo = testedCore.networkInfo + + // Then + assertThat(networkInfo).isSameAs(fakeNetworkInfo) + } + + @Test + fun `M provide last view event W lastViewEvent()`( + @Forgery fakeLastViewEvent: JsonObject + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.lastViewEvent) doReturn fakeLastViewEvent + + // When + val lastViewEvent = testedCore.lastViewEvent + + // Then + assertThat(lastViewEvent).isSameAs(fakeLastViewEvent) + } + + @Test + fun `M provide last fatal ANR sent W lastFatalAnrSent()`( + @LongForgery(min = 0L) fakeLastFatalAnrSent: Long + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.lastFatalAnrSent) doReturn fakeLastFatalAnrSent + + // When + val lastFatalAnrSent = testedCore.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isEqualTo(fakeLastFatalAnrSent) + } + + @Test + fun `M provide app start time W appStartTimeNs()`( + @LongForgery(min = 0L) fakeAppStartTimeNs: Long + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.appStartTimeNs) doReturn fakeAppStartTimeNs + + // When + val appStartTimeNs = testedCore.appStartTimeNs + + // Then + assertThat(appStartTimeNs).isEqualTo(fakeAppStartTimeNs) + } + + @Test + fun `M return tracking consent W trackingConsent()`( + @Forgery fakeTrackingConsent: TrackingConsent + ) { + // Given + val mockFuture = mock>() + whenever(mockFuture.get()) doReturn fakeTrackingConsent + whenever( + testedCore.coreFeature.contextExecutorService.submit(any>()) + ) doReturn mockFuture + + // When + val trackingConsent = testedCore.trackingConsent + + // When + Then + assertThat(trackingConsent).isEqualTo(fakeTrackingConsent) + } + + @Test + fun `M return default tracking consent W trackingConsent() { failed to get tracking consent }`( + forge: Forge + ) { + // Given + val mockFuture = mock>() + val fakeThrowable = forge.anElementFrom( + ExecutionException(forge.aThrowable()), + CancellationException(), + InterruptedException() + ) + whenever(mockFuture.get()) doThrow fakeThrowable + whenever( + testedCore.coreFeature.contextExecutorService.submit(any>()) + ) doReturn mockFuture + + // When + val trackingConsent = testedCore.trackingConsent + + // When + Then + assertThat(trackingConsent).isEqualTo(TrackingConsent.NOT_GRANTED) + } + + @Test + fun `M return root storage dir W rootStorageDir()`() { + // When + Then + assertThat(testedCore.rootStorageDir).isEqualTo(testedCore.coreFeature.storageDir) + } + + @Test + fun `M persist the event W writeLastViewEvent(){ NDK feature registered }`( + @StringForgery viewEvent: String + ) { + // Given + val fakeViewEvent = viewEvent.toByteArray() + testedCore.features += Feature.NDK_CRASH_REPORTS_FEATURE_NAME to mock() + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature + + // When + testedCore.writeLastViewEvent(fakeViewEvent) + + // Then + verify(mockCoreFeature).writeLastViewEvent(fakeViewEvent) + } + + @Test + fun `M persist the event W writeLastViewEvent(){ R+ }`( + @StringForgery viewEvent: String, + @IntForgery(min = Build.VERSION_CODES.R) fakeSdkVersion: Int + ) { + // Given + val fakeViewEvent = viewEvent.toByteArray() + whenever(mockBuildSdkVersionProvider.version) doReturn fakeSdkVersion + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature + + // When + testedCore.writeLastViewEvent(fakeViewEvent) + + // Then + verify(mockCoreFeature).writeLastViewEvent(fakeViewEvent) + } + + @Test + fun `M log info when writing last view event W writeLastViewEvent(){ below R and no NDK feature }`( + @StringForgery viewEvent: String, + @IntForgery(min = 1, max = Build.VERSION_CODES.R) fakeSdkVersion: Int + ) { + // Given + val mockCoreFeature = mock() + whenever(mockBuildSdkVersionProvider.version) doReturn fakeSdkVersion + testedCore.coreFeature = mockCoreFeature + reset(mockInternalLogger) + + // When + testedCore.writeLastViewEvent(viewEvent.toByteArray()) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.MAINTAINER, + DatadogCore.NO_NEED_TO_WRITE_LAST_VIEW_EVENT + ) + } + + @Test + fun `M delete last view event W deleteLastViewEvent()`() { + // Given + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature + + // When + testedCore.deleteLastViewEvent() + + // Then + verify(mockCoreFeature).deleteLastViewEvent() + } + + @Test + fun `M write last fatal ANR sent W writeLastFatalAnrSent()`( + @LongForgery(min = 0L) fakeLastFatalAnrSent: Long + ) { + // Given + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature + + // When + testedCore.writeLastFatalAnrSent(fakeLastFatalAnrSent) + + // Then + verify(mockCoreFeature).writeLastFatalAnrSent(fakeLastFatalAnrSent) + } + + @Test + fun `M clear data in all features W clearAllData()`( + forge: Forge + ) { + // Given + // there are some non-mock features there after initialization + testedCore.features.clear() + testedCore.features.putAll( + forge.aMap { + anAlphaNumericalString() to mock() + } + ) + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature + whenever(mockCoreFeature.persistenceExecutorService) doReturn mockPersistenceExecutorService + whenever(mockCoreFeature.contextExecutorService) doReturn mockContextExecutorService + + // When + testedCore.clearAllData() + + // Then + testedCore.features.forEach { + verify(it.value).clearAllData() + } + verify(mockCoreFeature).deleteLastFatalAnrSent() + verify(mockCoreFeature).deleteLastViewEvent() + } + + @Test + fun `M flush data in all features W flushStoredData()`( + forge: Forge + ) { + // Given + // there are some non-mock features there after initialization + testedCore.features.clear() + + testedCore.features.putAll( + forge.aMap { + anAlphaNumericalString() to mock() + } + ) + whenever(mockContextExecutorService.queue) doReturn LinkedBlockingQueue() + + // When + testedCore.flushStoredData() + + // Then + testedCore.features.forEach { + verify(it.value).flushStoredData() + } + } + + @Test + fun `M stop all features W stop()`( + @StringForgery fakeFeatureNames: Set + ) { + // Given + val mockCoreFeature = mock() + whenever(mockCoreFeature.initialized).thenReturn(mock()) + testedCore.coreFeature = mockCoreFeature + + val sdkFeatureMocks = fakeFeatureNames.map { + it to mock() + } + + sdkFeatureMocks.forEach { testedCore.features += it } + + // When + testedCore.stop() + + // Then + verify(mockCoreFeature).stop() + sdkFeatureMocks.forEach { + verify(it.second).stop() + } + + assertThat(testedCore.contextProvider).isInstanceOf(NoOpContextProvider::class.java) + assertThat(testedCore.isActive).isFalse + assertThat(testedCore.features).isEmpty() + } + + @Test + fun `M reset developer mode when stop()`() { + // Given + testedCore.isDeveloperModeEnabled = true + + // When + testedCore.stop() + + // Then + assertThat(testedCore.isDeveloperModeEnabled).isFalse() + } + + @Test + fun `M register process lifecycle monitor W initialize()`() { + // Then + argumentCaptor { + verify(appContext.mockInstance) + .registerActivityLifecycleCallbacks(capture()) + assertThat(lastValue).isInstanceOf(ProcessLifecycleMonitor::class.java) + val callback = (lastValue as ProcessLifecycleMonitor).callback + assertThat(callback).isInstanceOf(ProcessLifecycleCallback::class.java) + val processLifecycleCallback = callback as ProcessLifecycleCallback + assertThat(processLifecycleCallback.instanceName).isEqualTo(fakeInstanceName) + } + } + + @Test + fun `M unregister process lifecycle monitor W stop()`() { + // Given + val expectedInvocations = if (fakeConfiguration.crashReportsEnabled) 2 else 1 + + // When + testedCore.stop() + + // Then + argumentCaptor { + verify(appContext.mockInstance, times(expectedInvocations)) + .unregisterActivityLifecycleCallbacks(capture()) + assertThat(lastValue).isInstanceOf(ProcessLifecycleMonitor::class.java) + } + } + + @Test + fun `M allow concurrent access to features W access features when modifying their collection`( + @StringForgery fakeFeature: String, + forge: Forge + ) { + // Given + val mockFeature = mock() + whenever(mockFeature.featureContextLock) doReturn ReentrantReadWriteLock() + whenever(mockFeature.featureContext) doReturn mutableMapOf() + testedCore.features += fakeFeature to mockFeature + + // When + val errorCollector = Collections.synchronizedList(mutableListOf()) + val latch = CountDownLatch(2) + val threadA = Thread( + ErrorRecordingRunnable(errorCollector) { + latch.countDown() + latch.await() + assertDoesNotThrow { + repeat(100) { + testedCore.features += forge.anAlphabeticalString() to mock() + } + } + } + ).apply { start() } + val threadB = Thread( + ErrorRecordingRunnable(errorCollector) { + latch.countDown() + latch.await() + assertDoesNotThrow { + repeat(100) { + testedCore.updateFeatureContext(fakeFeature) { + // no-op + } + } + } + } + ).apply { start() } + + listOf(threadA, threadB).forEach { it.join() } + + // Then + if (errorCollector.isNotEmpty()) { + AssertionFailureBuilder + .assertionFailure() + .message( + "Expected no errors to be thrown during the concurrent" + + " access to features, but there were errors recorded. See first seen error below." + ) + .cause(errorCollector.first()) + .buildAndThrow() + } + } + + @Test + fun `M return false W isActiveCore() { CoreFeature inactive }`() { + // Given + val mockCoreFeature = mock() + whenever(mockCoreFeature.initialized).thenReturn(AtomicBoolean(false)) + testedCore.coreFeature = mockCoreFeature + + // When + val isActive = testedCore.isCoreActive() + + // Then + assertThat(isActive).isFalse() + } + + @Test + fun `M return true W isActiveCore() { CoreFeature active }`() { + // Given + val mockCoreFeature = mock() + whenever(mockCoreFeature.initialized).thenReturn(AtomicBoolean(true)) + testedCore.coreFeature = mockCoreFeature + + // When + val isActive = testedCore.isCoreActive() + + // Then + assertThat(isActive).isTrue() + } + + class ErrorRecordingRunnable( + private val collector: MutableList, + private val delegate: Runnable + ) : Runnable { + override fun run() { + try { + delegate.run() + } catch (t: Throwable) { + collector += t + } + } + } + + companion object { + + val msToNs = TimeUnit.MILLISECONDS.toNanos(1) + + val appContext = ApplicationContextTestConfiguration(Application::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/SdkReferenceTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/SdkReferenceTest.kt new file mode 100644 index 0000000000..7534e427eb --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/SdkReferenceTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core + +import com.datadog.android.Datadog +import com.datadog.android.core.internal.DatadogCore +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +internal class SdkReferenceTest { + + @Mock + lateinit var mockSdkCore: DatadogCore + + @BeforeEach + fun `set up`() { + Datadog.registry.register(null, mockSdkCore) + } + + @AfterEach + fun `tear down`() { + Datadog.registry.clear() + } + + @Test + fun `M return SDK instance W get() {instance exists}`() { + // Given + val testedReference = SdkReference(null) + + // When + val sdkCore = testedReference.get() + + // Then + assertThat(sdkCore).isSameAs(mockSdkCore) + } + + @Test + fun `M return null W get() {instance doesn't exist}`( + @StringForgery fakeInstanceName: String + ) { + // Given + val emptyReference = SdkReference(fakeInstanceName) + + // When + val sdkCore = emptyReference.get() + + // Then + assertThat(sdkCore).isNull() + } + + @Test + fun `M release reference W get() {instance is stopped}`() { + // Given + val testedReference = SdkReference(null) + assertThat(testedReference.get()).isNotNull + whenever(mockSdkCore.isActive) doReturn false + + // When + val sdkCore = testedReference.get() + + // Then + assertThat(sdkCore).isNull() + } + + @Test + fun `M call onSdkInstanceCaptured once W get() { multiple threads }`( + @IntForgery(min = 2, max = 10) threadCount: Int + ) { + // Given + var callsCount = 0 + val testedReference = SdkReference(null) { + callsCount++ + } + + // When + val threads = buildList(threadCount) { add(Thread { testedReference.get() }) } + threads.forEach { it.start() } + threads.forEach { it.join() } + + // Then + assertThat(callsCount).isEqualTo(1) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt new file mode 100644 index 0000000000..fd639a72d1 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt @@ -0,0 +1,461 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +import com.datadog.android.DatadogSite +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.security.Encryption +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.utils.config.InternalLoggerTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.ApiLevelExtension +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Authenticator +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import java.net.Proxy +import java.net.URL + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings +@ForgeConfiguration(value = Configurator::class) +internal class ConfigurationBuilderTest { + + lateinit var testedBuilder: Configuration.Builder + + @BeforeEach + fun `set up`(forge: Forge) { + testedBuilder = Configuration.Builder( + clientToken = forge.anHexadecimalString(), + env = forge.aStringMatching("[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]"), + variant = forge.anElementFrom(forge.anAlphabeticalString(), ""), + service = forge.aStringMatching("[a-z]+(\\.[a-z]+)+") + ) + } + + @Test + fun `M use sensible defaults W build()`() { + // When + val config = testedBuilder.build() + + // Then + assertThat(config.coreConfig.needsClearTextHttp).isFalse() + assertThat(config.coreConfig.enableDeveloperModeWhenDebuggable).isFalse() + assertThat(config.coreConfig.firstPartyHostsWithHeaderTypes).isEmpty() + assertThat(config.coreConfig.batchSize).isEqualTo(BatchSize.MEDIUM) + assertThat(config.coreConfig.uploadFrequency).isEqualTo(UploadFrequency.AVERAGE) + assertThat(config.coreConfig.proxy).isNull() + assertThat(config.coreConfig.proxyAuth).isEqualTo(Authenticator.NONE) + assertThat(config.coreConfig.encryption).isNull() + assertThat(config.coreConfig.site).isEqualTo(DatadogSite.US1) + assertThat(config.coreConfig.batchProcessingLevel).isEqualTo(BatchProcessingLevel.MEDIUM) + assertThat(config.coreConfig.persistenceStrategyFactory).isNull() + assertThat(config.coreConfig.backpressureStrategy.backpressureMitigation) + .isEqualTo(BackPressureMitigation.IGNORE_NEWEST) + assertThat(config.coreConfig.backpressureStrategy.capacity).isEqualTo(1024) + assertThat(config.crashReportsEnabled).isTrue + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M build config without crashReportConfig W build() { crashReports disabled }`( + forge: Forge + ) { + // Given + testedBuilder = Configuration.Builder( + clientToken = forge.anHexadecimalString(), + env = forge.aStringMatching("[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]"), + variant = forge.anElementFrom(forge.anAlphabeticalString(), ""), + service = forge.aStringMatching("[a-z]+(\\.[a-z]+)+") + ) + .setCrashReportsEnabled(false) + + // When + val config = testedBuilder.build() + + // Then + assertThat(config.crashReportsEnabled).isFalse + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M build config with custom site W useSite() and build()`( + @Forgery site: DatadogSite + ) { + // When + val config = testedBuilder.useSite(site).build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy(site = site) + ) + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M build config with first party hosts W setFirstPartyHosts() { ip addresses }`( + @StringForgery( + regex = "(([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])" + ) hosts: List + ) { + // When + val config = testedBuilder + .setFirstPartyHosts(hosts) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + firstPartyHostsWithHeaderTypes = + hosts.associateWith { + setOf( + TracingHeaderType.DATADOG, + TracingHeaderType.TRACECONTEXT + ) + } + ) + ) + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M build config with first party hosts W setFirstPartyHosts() { host names }`( + @StringForgery( + regex = "(([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])" + ) hosts: List + ) { + // When + val config = testedBuilder + .setFirstPartyHosts(hosts) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + firstPartyHostsWithHeaderTypes = hosts.associateWith { + setOf( + TracingHeaderType.DATADOG, + TracingHeaderType.TRACECONTEXT + ) + } + ) + ) + assertThat(config.crashReportsEnabled).isTrue + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M use url host name W setFirstPartyHosts() { url }`( + @StringForgery( + regex = "(https|http)://([a-z][a-z0-9-]{3,9}\\.){1,4}[a-z][a-z0-9]{2,3}" + ) hosts: List + ) { + // WHEN + val config = testedBuilder + .setFirstPartyHosts(hosts) + .build() + + // THEN + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + firstPartyHostsWithHeaderTypes = + hosts.associate { + URL(it).host to setOf( + TracingHeaderType.DATADOG, + TracingHeaderType.TRACECONTEXT + ) + } + ) + ) + assertThat(config.crashReportsEnabled).isTrue + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M sanitize hosts W setFirstPartyHosts()`( + @StringForgery( + regex = "(([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])" + ) hosts: List + ) { + // When + val mockSanitizer: HostsSanitizer = mock() + testedBuilder.hostsSanitizer = mockSanitizer + testedBuilder + .setFirstPartyHosts(hosts) + .build() + + // Then + verify(mockSanitizer) + .sanitizeHosts( + hosts, + Configuration.NETWORK_REQUESTS_TRACKING_FEATURE_NAME + ) + } + + @Test + fun `M build config with first party hosts and header types W setFirstPartyHostsWithHeaderType() { host names }`( + @StringForgery( + regex = "(([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])" + ) hosts: List, + forge: Forge + ) { + val hostWithHeaderTypes = hosts.associateWith { + forge.aList { aValueFrom(TracingHeaderType::class.java) }.toSet() + } + + // When + val config = testedBuilder + .setFirstPartyHostsWithHeaderType(hostWithHeaderTypes) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy(firstPartyHostsWithHeaderTypes = hostWithHeaderTypes) + ) + assertThat(config.crashReportsEnabled).isTrue + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M use batch size W setBatchSize()`( + @Forgery batchSize: BatchSize + ) { + // When + val config = testedBuilder + .setBatchSize(batchSize) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy(batchSize = batchSize) + ) + assertThat(config.crashReportsEnabled).isTrue + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M developer flag set W setUseDeveloperModeWhenDebuggable()`( + @BoolForgery enableDeveloperDebugInfo: Boolean + ) { + // When + val config = testedBuilder + .setUseDeveloperModeWhenDebuggable(enableDeveloperDebugInfo) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + enableDeveloperModeWhenDebuggable = enableDeveloperDebugInfo + ) + ) + } + + @Test + fun `M use upload frequency W setUploadFrequency()`( + @Forgery uploadFrequency: UploadFrequency + ) { + // When + val config = testedBuilder + .setUploadFrequency(uploadFrequency) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy(uploadFrequency = uploadFrequency) + ) + assertThat(config.crashReportsEnabled).isTrue + assertThat(config.additionalConfig).isEmpty() + } + + @Test + fun `M build with additionalConfig W setAdditionalConfiguration()`(forge: Forge) { + // Given + val additionalConfig = forge.aMap { + forge.anAsciiString() to forge.aString() + } + + // When + val config = testedBuilder + .setAdditionalConfiguration(additionalConfig) + .build() + + // Then + assertThat(config.additionalConfig).isEqualTo(additionalConfig) + + assertThat(config.coreConfig).isEqualTo(Configuration.DEFAULT_CORE_CONFIG) + assertThat(config.crashReportsEnabled).isTrue + } + + @Test + fun `M build config with Proxy and Auth configuration W setProxy() and build()`() { + // Given + val mockProxy: Proxy = mock() + val mockAuthenticator: Authenticator = mock() + + // When + val config = testedBuilder + .setProxy(mockProxy, mockAuthenticator) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + proxy = mockProxy, + proxyAuth = mockAuthenticator + ) + ) + } + + @Test + fun `M build config with Proxy configuration W setProxy() and build()`() { + // Given + val mockProxy: Proxy = mock() + + // When + val config = testedBuilder + .setProxy(mockProxy, null) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + proxy = mockProxy, + proxyAuth = Authenticator.NONE + ) + ) + } + + @Test + fun `M build config with security configuration W setEncryption() and build()`() { + // Given + val mockEncryption = mock() + + // When + val config = testedBuilder + .setEncryption(mockEncryption) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + encryption = mockEncryption + ) + ) + } + + @Test + fun `M build config with persistence strategy W setPersistenceStrategyFactory() and build()`() { + // Given + val mockFactory = mock() + + // When + val config = testedBuilder + .setPersistenceStrategyFactory(mockFactory) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + persistenceStrategyFactory = mockFactory + ) + ) + } + + @Test + fun `M build config with BackPressure strategy W setBackpressureStrategy() and build()`( + @IntForgery capacity: Int, + @Forgery mitigation: BackPressureMitigation + ) { + // Given + val fakeBackpressureStrategy = BackPressureStrategy( + capacity, + mock<() -> Unit>(), + mock<(Any) -> Unit>(), + mitigation + ) + + // When + val config = testedBuilder + .setBackpressureStrategy(fakeBackpressureStrategy) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + backpressureStrategy = fakeBackpressureStrategy + ) + ) + } + + @Test + fun `M build config with UploadScheduler strategy W setUploadSchedulerStrategy() and build()`() { + // Given + val mockUploadSchedulerStrategy: UploadSchedulerStrategy = mock() + + // When + val config = testedBuilder + .setUploadSchedulerStrategy(mockUploadSchedulerStrategy) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + uploadSchedulerStrategy = mockUploadSchedulerStrategy + ) + ) + } + + @Test + fun `M build config with allowClearTextHttp W allowClearTextHttp() and build()`() { + // When + val config = testedBuilder + .allowClearTextHttp() + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy( + needsClearTextHttp = true + ) + ) + } + + companion object { + val logger = InternalLoggerTestConfiguration() + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(logger) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/HostsSanitizerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/HostsSanitizerTest.kt new file mode 100644 index 0000000000..39eba7456a --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/HostsSanitizerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.configuration + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.config.InternalLoggerTestConfiguration +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import java.net.MalformedURLException +import java.net.URL +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +internal class HostsSanitizerTest { + + lateinit var testedSanitizer: HostsSanitizer + + @StringForgery(type = StringForgeryType.ALPHABETICAL) + lateinit var fakeFeature: String + + @BeforeEach + fun `set up`() { + testedSanitizer = HostsSanitizer() + } + + @Test + fun `M return the whole hosts list W valid IPs list provided`( + @StringForgery( + regex = "(([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])" + ) hosts: List + ) { + // When + val validHosts = testedSanitizer.sanitizeHosts(hosts, fakeFeature) + + // Then + assertThat(validHosts).isEqualTo(hosts) + } + + @Test + fun `M return the whole hosts list W sanitizeHosts { valid hosts used }`( + @StringForgery( + regex = "(([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])" + ) hosts: List + ) { + // When + val validHosts = testedSanitizer.sanitizeHosts(hosts, fakeFeature) + + // Then + assertThat(validHosts).isEqualTo(hosts) + } + + @Test + fun `M filter out everything W sanitizeHosts { using top level domains only }`( + @StringForgery( + regex = "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" + ) hosts: List + ) { + // When + val filtered = testedSanitizer.sanitizeHosts(hosts, fakeFeature) + + // Then + assertThat(filtered).isEmpty() + } + + @Test + fun `M return only localhost W sanitizeHosts { using top level domains and localhost }`( + @StringForgery( + regex = "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" + ) hosts: List, + forge: Forge + ) { + // Given + val fakeLocalHost = forge.aStringMatching("localhost|LOCALHOST") + + // When + val sanitizedHosts = testedSanitizer.sanitizeHosts( + hosts + fakeLocalHost, + fakeFeature + ) + + // Then + assertThat(sanitizedHosts).containsOnly(fakeLocalHost) + } + + @Test + fun `M log error W sanitizeHosts { malformed hostname }`( + @StringForgery( + regex = "(([-+=~> + ) { + // When + testedSanitizer.sanitizeHosts( + hosts, + fakeFeature + ) + + // Then + val expectedMessages = hosts.map { + HostsSanitizer.ERROR_MALFORMED_HOST_IP_ADDRESS.format( + Locale.US, + it, + fakeFeature + ) + } + argumentCaptor<() -> String> { + verify(logger.mockInternalLogger, times(hosts.size)).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(allValues.map { it() }) + .containsExactlyInAnyOrderElementsOf(expectedMessages) + } + } + + @Test + fun `M log error W sanitizeHosts { malformed ip address }`( + @StringForgery( + regex = "(([0-9]{3}\\.){3}[0.9]{4})" + + "|(([0-9]{4,9}\\.)[0.9]{4})" + + "|(25[6-9]\\.([0-9]{3}\\.){2}[0.9]{3})" + ) hosts: List + ) { + // When + testedSanitizer.sanitizeHosts( + hosts, + fakeFeature + ) + + // THEN + val expectedMessages = hosts.map { + HostsSanitizer.ERROR_MALFORMED_HOST_IP_ADDRESS.format( + Locale.US, + it, + fakeFeature + ) + } + argumentCaptor<() -> String> { + verify(logger.mockInternalLogger, times(hosts.size)).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(allValues.map { it() }) + .containsExactlyInAnyOrderElementsOf(expectedMessages) + } + } + + @Test + fun `M drop all malformed hosts W sanitizeHosts { malformed hostname }`( + @StringForgery( + regex = "(([-+=~> + ) { + // When + val sanitizedHosts = testedSanitizer.sanitizeHosts( + hosts, + fakeFeature + ) + + // Then + assertThat(sanitizedHosts).isEmpty() + } + + @Test + fun `M drop all malformed ip addresses W sanitizeHosts { malformed ip address }`( + @StringForgery( + regex = "(([0-9]{3}\\.){3}[0.9]{4})" + + "|(([0-9]{4,9}\\.)[0.9]{4})" + + "|(25[6-9]\\.([0-9]{3}\\.){2}[0.9]{3})" + ) hosts: List + ) { + // When + val sanitizedHosts = testedSanitizer.sanitizeHosts( + hosts, + fakeFeature + ) + + // Then + assertThat(sanitizedHosts).isEmpty() + } + + @Test + fun `M use url host name W sanitizeHosts { url }`( + @StringForgery( + regex = "(https|http)://([a-z][a-z0-9-]{3,9}\\.){1,4}[a-z][a-z0-9]{2,3}" + ) hosts: List + ) { + // When + val sanitizedHosts = testedSanitizer.sanitizeHosts( + hosts, + fakeFeature + ) + + // Then + assertThat(sanitizedHosts).isEqualTo(hosts.map { URL(it).host }) + } + + @Test + fun `M warn W sanitizeHosts { url }`( + @StringForgery( + regex = "([a-z][a-z0-9-]{3,9}\\.){1,4}[a-z][a-z0-9]{2,3}" + ) hosts: List + ) { + // Given + val urls = hosts.map { "https://$it/" } + + // When + testedSanitizer.sanitizeHosts(urls, fakeFeature) + + // THEN + val expectedMessages = hosts.map { + HostsSanitizer.WARNING_USING_URL.format( + Locale.US, + "https://$it/", + fakeFeature, + it + ) + } + argumentCaptor<() -> String> { + verify(logger.mockInternalLogger, times(hosts.size)).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(allValues.map { it() }) + .containsExactlyInAnyOrderElementsOf(expectedMessages) + } + } + + @Test + fun `M warn W sanitizeHosts { malformed url }`( + @StringForgery( + regex = "(https|http)://([a-z][a-z0-9-]{3,9}\\.){1,4}[a-z][a-z0-9]{2,3}:-8[0-9]{1}" + ) hosts: List + ) { + // When + testedSanitizer.sanitizeHosts( + hosts, + fakeFeature + ) + + // Then + val expectedMessages = hosts.map { + HostsSanitizer.ERROR_MALFORMED_URL.format( + Locale.US, + it, + fakeFeature + ) + } + argumentCaptor<() -> String> { + verify(logger.mockInternalLogger, times(hosts.size)).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.USER), + capture(), + any(), + eq(false), + eq(null) + ) + assertThat(allValues.map { it() }) + .containsExactlyInAnyOrderElementsOf(expectedMessages) + } + } + + @Test + fun `M drop all malformed urls W sanitizeHosts`( + @StringForgery( + regex = "(https|http)://([a-z][a-z0-9-]{3,9}\\.){1,4}[a-z][a-z0-9]{2,3}:-8[0-9]{1}" + ) hosts: List + ) { + // When + val sanitizedHosts = testedSanitizer.sanitizeHosts( + hosts, + fakeFeature + ) + + // Then + assertThat(sanitizedHosts).isEmpty() + } + + companion object { + val logger = InternalLoggerTestConfiguration() + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(logger) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/constraints/DatadogDataConstraintsTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/constraints/DatadogDataConstraintsTest.kt new file mode 100644 index 0000000000..397c088db1 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/constraints/DatadogDataConstraintsTest.kt @@ -0,0 +1,409 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.constraints + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.times +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Case +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings() +@ForgeConfiguration(Configurator::class) +internal class DatadogDataConstraintsTest { + + lateinit var testedConstraints: DataConstraints + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedConstraints = DatadogDataConstraints(mockInternalLogger) + } + + // region Tags + + @Test + fun `keep valid tag`(forge: Forge) { + val tag = forge.aStringMatching("[a-z]([a-z0-9_:./-]{0,198}[a-z0-9_./-])?") + + val result = testedConstraints.validateTags(listOf(tag)) + + assertThat(result).containsOnly(tag) + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `ignore invalid tag - start with a letter`(forge: Forge) { + val key = forge.aStringMatching("\\d[a-z]+") + val value = forge.aNumericalString() + val tag = "$key:$value" + + val result = testedConstraints.validateTags(listOf(tag)) + + assertThat(result).isEmpty() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "\"$tag\" is an invalid tag, and was ignored." + ) + } + + @Test + fun `replace illegal characters`(forge: Forge) { + val validPart = forge.anAlphabeticalString(size = 3) + val invalidPart = forge.aString { + anElementFrom(',', '?', '%', '(', ')', '[', ']', '{', '}') + } + val value = forge.aNumericalString() + val tag = "$validPart$invalidPart:$value" + + val result = testedConstraints.validateTags(listOf(tag)) + + val converted = '_' * invalidPart.length + val expectedCorrectedTag = "$validPart$converted:$value" + assertThat(result) + .containsOnly(expectedCorrectedTag) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "tag \"$tag\" was modified to \"$expectedCorrectedTag\" to match our constraints.", + onlyOnce = true + ) + } + + @Test + fun `convert uppercase key to lowercase`(forge: Forge) { + val key = forge.anAlphabeticalString(case = Case.UPPER) + val value = forge.aNumericalString() + val tag = "$key:$value" + + val result = testedConstraints.validateTags(listOf(tag)) + + val expectedCorrectedTag = "${key.lowercase(Locale.US)}:$value" + assertThat(result) + .containsOnly(expectedCorrectedTag) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "tag \"$tag\" was modified to \"$expectedCorrectedTag\" to match our constraints.", + onlyOnce = true + ) + } + + @Test + fun `trim tags over 200 characters`(forge: Forge) { + val tag = forge.anAlphabeticalString(size = forge.aSmallInt() + 200) + + val result = testedConstraints.validateTags(listOf(tag)) + + val expectedCorrectedTag = tag.substring(0, 200) + assertThat(result) + .containsOnly(expectedCorrectedTag) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "tag \"$tag\" was modified to \"$expectedCorrectedTag\" to match our constraints.", + onlyOnce = true + ) + } + + @Test + fun `trim tags ending with a colon`(forge: Forge) { + val expectedCorrectedTag = forge.anAlphabeticalString() + + val result = testedConstraints.validateTags(listOf("$expectedCorrectedTag:")) + + assertThat(result) + .containsOnly(expectedCorrectedTag) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "tag \"$expectedCorrectedTag:\" was modified to " + + "\"$expectedCorrectedTag\" to match our constraints.", + onlyOnce = true + ) + } + + @Test + fun `ignore reserved tag keys`(forge: Forge) { + val key = forge.anElementFrom("host", "device", "source", "service") + val value = forge.aNumericalString() + val invalidTag = "$key:$value" + + val result = testedConstraints.validateTags(listOf(invalidTag)) + + assertThat(result) + .isEmpty() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "\"$invalidTag\" is an invalid tag, and was ignored." + ) + } + + @Test + fun `ignore reserved tag keys (workaround)`(forge: Forge) { + val key = forge.randomizeCase { anElementFrom("host", "device", "source", "service") } + val value = forge.aNumericalString() + val invalidTag = "$key:$value" + + val result = testedConstraints.validateTags(listOf(invalidTag)) + + assertThat(result) + .isEmpty() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "\"$invalidTag\" is an invalid tag, and was ignored." + ) + } + + @Test + fun `ignore tag if adding more than 100`(forge: Forge) { + val tags = forge.aList(128) { aStringMatching("[a-z]{1,8}:[0-9]{1,8}") } + val firstTags = tags.take(100) + + val result = testedConstraints.validateTags(tags) + + val discardedCount = tags.size - 100 + assertThat(result) + .containsExactlyElementsOf(firstTags) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "too many tags were added, $discardedCount had to be discarded." + ) + } + + //endregion + + // region Attributes + + @Test + fun `keep valid attribute`( + forge: Forge + ) { + val key = forge.anAlphabeticalString() + val value = forge.aNumericalString() + + val result = testedConstraints.validateAttributes(mapOf(key to value)) + + assertThat(result) + .containsEntry(key, value) + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M convert nested attribute keys W over 10 levels`(forge: Forge) { + val topLevels = forge.aList(10) { anAlphabeticalString() } + val lowerLevels = forge.aList { anAlphabeticalString() } + val key = (topLevels + lowerLevels).joinToString(".") + val value = forge.aNumericalString() + + val result = testedConstraints.validateAttributes(mapOf(key to value)) + + val expectedKey = topLevels.joinToString(".") + "_" + lowerLevels.joinToString("_") + assertThat(result) + .containsEntry(expectedKey, value) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "Key \"$key\" was modified to \"$expectedKey\" to match our constraints." + ) + } + + @Test + fun `M convert nested attribute keys W over 10 levels and using prefix`(forge: Forge) { + val keyPrefix = forge + .aList(5) { forge.anAlphabeticalString() } + .joinToString(".") + val topLevels = forge.aList(5) { anAlphabeticalString() } + val lowerLevels = forge.aList { anAlphabeticalString() } + val key = (topLevels + lowerLevels).joinToString(".") + val value = forge.aNumericalString() + val result = + testedConstraints.validateAttributes( + mapOf(key to value), + keyPrefix = keyPrefix + ) + + val expectedKey = topLevels.joinToString(".") + "_" + lowerLevels.joinToString("_") + assertThat(result) + .containsEntry(expectedKey, value) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "Key \"$key\" was modified to \"$expectedKey\" to match our constraints." + ) + } + + @Test + fun `ignore attribute if adding more than 128`(forge: Forge) { + val attributes = forge.aList(202) { anAlphabeticalString() to anInt() }.toMap() + val firstAttributes = attributes.toList().take(128).toMap() + + val result = testedConstraints.validateAttributes(attributes) + + val discardedCount = attributes.size - 128 + assertThat(result) + .hasSize(128) + .containsAllEntriesOf(firstAttributes) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "Too many attributes were added, $discardedCount had to be discarded." + ) + } + + @Test + fun `M use a custom error format W validateAttributes`(forge: Forge) { + val attributes = forge.aList(202) { anAlphabeticalString() to anInt() }.toMap() + val firstAttributes = attributes.toList().take(128).toMap() + val fakeAttributesGroup = forge + .aList(size = 10) { forge.anAlphabeticalString() } + .joinToString(".") + val result = testedConstraints.validateAttributes( + attributes, + attributesGroupName = fakeAttributesGroup + ) + + val discardedCount = attributes.size - 128 + assertThat(result) + .hasSize(128) + .containsAllEntriesOf(firstAttributes) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "Too many attributes were added for [$fakeAttributesGroup]," + + " $discardedCount had to be discarded." + ) + } + + @Test + fun `M drop the reserved attributes W validateAttributes { reservedKeys provided }`( + forge: Forge + ) { + // GIVEN + val attributes = forge.aMap(size = 10) { + forge.anAlphaNumericalString() to forge.anAlphabeticalString() + } + val reservedKeys = attributes.keys.take(2).toHashSet() + + // WHEN + val sanitizedAttributes = + testedConstraints.validateAttributes(attributes, reservedKeys = reservedKeys) + + // THEN + assertThat(sanitizedAttributes) + .containsExactlyEntriesOf(attributes.filterNot { reservedKeys.contains(it.key) }) + } + + @Test + fun `M drop the reserved attributes W validateAttributes { null keys }`( + @StringForgery value: String + ) { + // GIVEN + val attributes = mapOf(null to value) as Map<*, *> + + @Suppress("UNCHECKED_CAST") // simulate an unsafe map sent from Java + val unsafeAttributes = attributes as Map + + // WHEN + + val sanitizedAttributes = testedConstraints.validateAttributes(unsafeAttributes) + + // THEN + assertThat(sanitizedAttributes).isEmpty() + } + + // endregion + + // region Events + + @Test + fun `M sanitize custom timings and log warning W validateEvent`( + forge: Forge + ) { + // Given + val expectedSanitizedKeys = mutableSetOf() + val badToSanitizedKeys = mutableMapOf() + + val customTimings = forge.aMap { + val goodTimingPart = forge.anAlphabeticalString(case = Case.ANY) + if (forge.aBool()) { + expectedSanitizedKeys.add(goodTimingPart) + goodTimingPart to forge.aLong() + } else { + val badTimingPart = forge.anAsciiString() + .replace(Regex("[a-zA-Z0-9\\-_.@$]"), forge.anElementFrom("%", "*", "!", "&")) + + val badKey = goodTimingPart + badTimingPart + val sanitizedKey = goodTimingPart + "_".repeat(badTimingPart.length) + + expectedSanitizedKeys.add(sanitizedKey) + badToSanitizedKeys[badKey] = sanitizedKey + + badKey to forge.aLong() + } + } + val expectedLogs = badToSanitizedKeys.toList().map { + DatadogDataConstraints.CUSTOM_TIMING_KEY_REPLACED_WARNING.format( + Locale.US, + it.first, + it.second + ) + } + + // When + val sanitizedTimings = testedConstraints.validateTimings(customTimings) + + // Then + assertThat(sanitizedTimings.keys) + .isEqualTo(expectedSanitizedKeys) + + argumentCaptor<() -> String> { + verify(mockInternalLogger, times(badToSanitizedKeys.size)).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + + assertThat(allValues.map { it() }) + .containsExactlyInAnyOrderElementsOf(expectedLogs) + } + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt new file mode 100644 index 0000000000..40bc433e92 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt @@ -0,0 +1,1538 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.os.Build +import android.os.Process +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider +import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider +import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.internal.privacy.NoOpConsentProvider +import com.datadog.android.core.internal.privacy.TrackingConsentProvider +import com.datadog.android.core.internal.system.BroadcastReceiverSystemInfoProvider +import com.datadog.android.core.internal.system.NoOpSystemInfoProvider +import com.datadog.android.core.internal.thread.BackPressuredBlockingQueue +import com.datadog.android.core.internal.time.AppStartTimeProvider +import com.datadog.android.core.internal.time.KronosTimeProvider +import com.datadog.android.core.internal.user.DatadogUserInfoProvider +import com.datadog.android.core.internal.user.NoOpMutableUserInfoProvider +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.core.thread.FlushableExecutorService +import com.datadog.android.internal.time.DefaultTimeProvider +import com.datadog.android.ndk.internal.DatadogNdkCrashHandler +import com.datadog.android.ndk.internal.NoOpNdkCrashHandler +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.security.Encryption +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.tools.unit.assertj.containsInstanceOf +import com.datadog.tools.unit.extensions.ApiLevelExtension +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Authenticator +import okhttp3.CipherSuite +import okhttp3.ConnectionSpec +import okhttp3.Protocol +import okhttp3.TlsVersion +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.isA +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.net.Proxy +import java.util.Locale +import java.util.UUID +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import kotlin.experimental.xor + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class CoreFeatureTest { + + private lateinit var testedFeature: CoreFeature + + @Mock + lateinit var mockConnectivityMgr: ConnectivityManager + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockPersistenceExecutorService: FlushableExecutorService + + @Mock + lateinit var mockScheduledExecutorService: ScheduledExecutorService + + @Mock + lateinit var mockAppStartTimeProvider: AppStartTimeProvider + + @Forgery + lateinit var fakeConfig: Configuration + + @Forgery + lateinit var fakeConsent: TrackingConsent + + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) + lateinit var fakeSdkInstanceId: String + + @Forgery + lateinit var fakeBuildId: UUID + + @BeforeEach + fun `set up`() { + CoreFeature.disableKronosBackgroundSync = true + testedFeature = CoreFeature( + mockInternalLogger, + mockAppStartTimeProvider, + executorServiceFactory = { _, _, _ -> mockPersistenceExecutorService }, + scheduledExecutorServiceFactory = { _, _, _ -> mockScheduledExecutorService } + ) + whenever(appContext.mockInstance.getSystemService(Context.CONNECTIVITY_SERVICE)) + .doReturn(mockConnectivityMgr) + whenever( + appContext.mockInstance.assets.open(CoreFeature.BUILD_ID_FILE_NAME) + ) doReturn fakeBuildId.toString().byteInputStream() + whenever(mockPersistenceExecutorService.execute(any())) doAnswer { + it.getArgument(0).run() + } + } + + @AfterEach + fun `tear down`() { + testedFeature.stop() + } + + // region initialization + + @Test + fun `M initialize time sync W initialize`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.kronosClock).isNotNull() + } + + @Test + fun `M initialize time provider W initialize`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.timeProvider) + .isInstanceOf(KronosTimeProvider::class.java) + } + + @Test + fun `M initialize system info provider W initialize`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.systemInfoProvider) + .isInstanceOf(BroadcastReceiverSystemInfoProvider::class.java) + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + @Test + @Disabled // RUM-10684: ApiLevelExtension is not able to set API level property + fun `M initialize network info provider W initialize`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + argumentCaptor { + verify(appContext.mockInstance, atLeastOnce()) + .registerReceiver(capture(), any()) + + assertThat(allValues) + .containsInstanceOf(BroadcastReceiverSystemInfoProvider::class.java) + .containsInstanceOf(BroadcastReceiverNetworkInfoProvider::class.java) + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + @Test + @TestTargetApi(Build.VERSION_CODES.N) + @Disabled // RUM-10684: ApiLevelExtension is not able to set API level property + fun `M initialize network info provider W initialize {N}`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + argumentCaptor { + verify(appContext.mockInstance, atLeastOnce()) + .registerReceiver(capture(), any()) + + assertThat(allValues) + .containsInstanceOf(BroadcastReceiverSystemInfoProvider::class.java) + assertThat(allValues.none { it is BroadcastReceiverNetworkInfoProvider }) + .isTrue + verify(mockConnectivityMgr) + .registerDefaultNetworkCallback(isA()) + } + } + + @Test + fun `M initialize user info provider W initialize`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.userInfoProvider) + .isInstanceOf(DatadogUserInfoProvider::class.java) + } + + @Test + fun `M initialise the consent provider W initialize`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.trackingConsentProvider) + .isInstanceOf(TrackingConsentProvider::class.java) + assertThat(testedFeature.trackingConsentProvider.getConsent()) + .isEqualTo(fakeConsent) + } + + @Test + fun `M initializes first party hosts resolver W initialize`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.firstPartyHostHeaderTypeResolver.knownHosts.keys) + .containsAll( + fakeConfig.coreConfig.firstPartyHostsWithHeaderTypes.keys.map { + it.lowercase( + Locale.US + ) + } + ) + } + + @Test + fun `M initializes app info W initialize()`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.clientToken).isEqualTo(fakeConfig.clientToken) + assertThat(testedFeature.packageVersionProvider.version).isEqualTo(appContext.fakeVersionName) + assertThat(testedFeature.serviceName).isEqualTo(fakeConfig.service) + assertThat(testedFeature.envName).isEqualTo(fakeConfig.env) + assertThat(testedFeature.variant).isEqualTo(fakeConfig.variant) + assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) + assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) + assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.TIRAMISU) + fun `M initializes app info W initialize() { TIRAMISU }`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.clientToken).isEqualTo(fakeConfig.clientToken) + assertThat(testedFeature.packageVersionProvider.version) + .isEqualTo(appContext.fakeVersionName) + assertThat(testedFeature.serviceName).isEqualTo(fakeConfig.service) + assertThat(testedFeature.envName).isEqualTo(fakeConfig.env) + assertThat(testedFeature.variant).isEqualTo(fakeConfig.variant) + assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) + assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) + assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + } + + @Test + fun `M initializes app info W initialize() {null serviceName}`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig.copy(service = null), + fakeConsent + ) + + // Then + assertThat(testedFeature.clientToken).isEqualTo(fakeConfig.clientToken) + assertThat(testedFeature.packageVersionProvider.version) + .isEqualTo(appContext.fakeVersionName) + assertThat(testedFeature.serviceName).isEqualTo(appContext.fakePackageName) + assertThat(testedFeature.envName).isEqualTo(fakeConfig.env) + assertThat(testedFeature.variant).isEqualTo(fakeConfig.variant) + assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) + assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) + assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + } + + @Test + fun `M initializes app info W initialize() {null versionName}`() { + // Given + appContext.fakePackageInfo.versionName = null + whenever(appContext.mockInstance.getSystemService(Context.CONNECTIVITY_SERVICE)) + .doReturn(mockConnectivityMgr) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.clientToken).isEqualTo(fakeConfig.clientToken) + assertThat(testedFeature.packageVersionProvider.version) + .isEqualTo(appContext.fakeVersionCode.toString()) + assertThat(testedFeature.serviceName).isEqualTo(fakeConfig.service) + assertThat(testedFeature.envName).isEqualTo(fakeConfig.env) + assertThat(testedFeature.variant).isEqualTo(fakeConfig.variant) + assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) + assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) + assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + } + + @Test + fun `M initializes app info W initialize() {unknown package name}`() { + // Given + @Suppress("DEPRECATION") + whenever(appContext.mockPackageManager.getPackageInfo(appContext.fakePackageName, 0)) + .doThrow(PackageManager.NameNotFoundException()) + whenever(appContext.mockInstance.getSystemService(Context.CONNECTIVITY_SERVICE)) + .doReturn(mockConnectivityMgr) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.clientToken).isEqualTo(fakeConfig.clientToken) + assertThat(testedFeature.packageVersionProvider.version) + .isEqualTo(CoreFeature.DEFAULT_APP_VERSION) + assertThat(testedFeature.serviceName).isEqualTo(fakeConfig.service) + assertThat(testedFeature.envName).isEqualTo(fakeConfig.env) + assertThat(testedFeature.variant).isEqualTo(fakeConfig.variant) + assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) + assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) + assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.TIRAMISU) + fun `M initializes app info W initialize() {unknown package name, TIRAMISU}`() { + // Given + whenever( + appContext.mockPackageManager.getPackageInfo( + appContext.fakePackageName, + PackageManager.PackageInfoFlags.of(0) + ) + ) + .doThrow(PackageManager.NameNotFoundException()) + whenever(appContext.mockInstance.getSystemService(Context.CONNECTIVITY_SERVICE)) + .doReturn(mockConnectivityMgr) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.clientToken).isEqualTo(fakeConfig.clientToken) + assertThat(testedFeature.packageVersionProvider.version).isEqualTo( + CoreFeature.DEFAULT_APP_VERSION + ) + assertThat(testedFeature.serviceName).isEqualTo(fakeConfig.service) + assertThat(testedFeature.envName).isEqualTo(fakeConfig.env) + assertThat(testedFeature.variant).isEqualTo(fakeConfig.variant) + assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) + assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) + assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + } + + @Test + fun `M initializes build ID W initialize()`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.appBuildId).isEqualTo(fakeBuildId.toString()) + } + + @Test + fun `M initializes build ID W initialize() { asset manager is closed }`() { + // Given + whenever( + appContext.mockInstance.assets.open(CoreFeature.BUILD_ID_FILE_NAME) + ) doThrow RuntimeException() + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.appBuildId).isNull() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + message = CoreFeature.BUILD_ID_READ_ERROR, + throwableClass = RuntimeException::class.java + ) + } + + @Test + fun `M initializes build ID W initialize() { build ID file is missing }`() { + // Given + whenever( + appContext.mockInstance.assets.open(CoreFeature.BUILD_ID_FILE_NAME) + ) doThrow FileNotFoundException() + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.appBuildId).isNull() + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + CoreFeature.BUILD_ID_IS_MISSING_INFO_MESSAGE + ) + } + + @Test + fun `M initializes build ID W initialize() { IOException during build ID read }`() { + // Given + val mockBrokenStream = mock().apply { + whenever(read(any())) doThrow IOException() + } + whenever( + appContext.mockInstance.assets.open(CoreFeature.BUILD_ID_FILE_NAME) + ) doReturn mockBrokenStream + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.appBuildId).isNull() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + message = CoreFeature.BUILD_ID_READ_ERROR, + throwableClass = IOException::class.java + ) + } + + @Test + fun `M initialize okhttp only once`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig.copy(coreConfig = fakeConfig.coreConfig), + fakeConsent + ) + + // Then + val okHttpClient = testedFeature.callFactory.okhttpClient + assertThat(okHttpClient).isEqualTo(testedFeature.callFactory.okhttpClient) + } + + @Test + fun `M initialize okhttp with strict network policy W initialize()`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig.copy(coreConfig = fakeConfig.coreConfig.copy(needsClearTextHttp = false)), + fakeConsent + ) + + // Then + val okHttpClient = testedFeature.callFactory.okhttpClient + assertThat(okHttpClient.protocols) + .containsExactly(Protocol.HTTP_2, Protocol.HTTP_1_1) + assertThat(okHttpClient.callTimeoutMillis) + .isEqualTo(CoreFeature.NETWORK_TIMEOUT_MS.toInt()) + assertThat(okHttpClient.connectionSpecs) + .hasSize(1) + + val connectionSpec = okHttpClient.connectionSpecs.first() + + assertThat(connectionSpec.isTls).isTrue() + assertThat(connectionSpec.tlsVersions) + .containsExactly(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3) + assertThat(connectionSpec.cipherSuites).containsExactly( + CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + ) + } + + @Test + fun `M initialize okhttp with no network policy W initialize() {needsClearText}`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig.copy(coreConfig = fakeConfig.coreConfig.copy(needsClearTextHttp = true)), + fakeConsent + ) + + // Then + val okHttpClient = testedFeature.callFactory.okhttpClient + assertThat(okHttpClient.protocols) + .containsExactly(Protocol.HTTP_2, Protocol.HTTP_1_1) + assertThat(okHttpClient.callTimeoutMillis) + .isEqualTo(CoreFeature.NETWORK_TIMEOUT_MS.toInt()) + assertThat(okHttpClient.connectionSpecs) + .containsExactly(ConnectionSpec.CLEARTEXT) + } + + @Test + fun `M initialize okhttp with proxy W initialize() {proxy configured}`() { + // When + val proxy: Proxy = mock() + val proxyAuth: Authenticator = mock() + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig.copy( + coreConfig = fakeConfig.coreConfig.copy( + proxy = proxy, + proxyAuth = proxyAuth + ) + ), + fakeConsent + ) + + // Then + val okHttpClient = testedFeature.callFactory.okhttpClient + assertThat(okHttpClient.proxy).isSameAs(proxy) + assertThat(okHttpClient.proxyAuthenticator).isSameAs(proxyAuth) + } + + @Test + fun `M initialize okhttp without proxy W initialize() {proxy not configured}`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig.copy(coreConfig = fakeConfig.coreConfig.copy(proxy = null)), + fakeConsent + ) + + // Then + val okHttpClient = testedFeature.callFactory.okhttpClient + assertThat(okHttpClient.proxy).isNull() + assertThat(okHttpClient.proxyAuthenticator).isEqualTo(Authenticator.NONE) + } + + @Test + fun `M initialize executors W initialize()`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.uploadExecutorService).isNotNull() + assertThat(testedFeature.persistenceExecutorService).isNotNull() + assertThat(testedFeature.contextExecutorService).isNotNull() + } + + @Test + fun `M initialize context executor with unbounded + observable queue W initialize()`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.contextExecutorService).isNotNull() + with(testedFeature.contextExecutorService.queue) { + check(this is BackPressuredBlockingQueue) + assertThat(capacity).isEqualTo(Int.MAX_VALUE) + } + } + + @Test + fun `M initialize only once W initialize() twice`( + @Forgery otherConfig: Configuration + ) { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + otherConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.clientToken).isEqualTo(fakeConfig.clientToken) + assertThat(testedFeature.packageVersionProvider.version) + .isEqualTo(appContext.fakeVersionName) + assertThat(testedFeature.serviceName).isEqualTo(fakeConfig.service) + assertThat(testedFeature.envName).isEqualTo(fakeConfig.env) + assertThat(testedFeature.variant).isEqualTo(fakeConfig.variant) + assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) + assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) + assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + } + + @Test + fun `M detect current process W initialize() {main process}`( + @StringForgery otherProcessName: String + ) { + // Given + val mockActivityManager = mock() + whenever(appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( + mockActivityManager + ) + val myProcess = forgeAppProcessInfo( + Process.myPid(), + appContext.fakePackageName + ) + val otherProcess = forgeAppProcessInfo( + Process.myPid() + 1, + otherProcessName + ) + whenever(mockActivityManager.runningAppProcesses) + .thenReturn(listOf(myProcess, otherProcess)) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.isMainProcess).isTrue() + } + + @Test + fun `M detect current process W initialize() {secondary process}`( + @StringForgery otherProcessName: String + ) { + // Given + val mockActivityManager = mock() + whenever(appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( + mockActivityManager + ) + val myProcess = forgeAppProcessInfo(Process.myPid(), otherProcessName) + val otherProcess = forgeAppProcessInfo( + Process.myPid() + 1, + appContext.fakePackageName + ) + whenever(mockActivityManager.runningAppProcesses) + .thenReturn(listOf(myProcess, otherProcess)) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.isMainProcess).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + CoreFeature.SDK_INITIALIZED_IN_SECONDARY_PROCESS_WARNING_MESSAGE + ) + } + + @Test + fun `M detect current process W initialize() {unknown process}`( + @StringForgery otherProcessName: String + ) { + // Given + val mockActivityManager = mock() + whenever(appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( + mockActivityManager + ) + val otherProcess = forgeAppProcessInfo( + Process.myPid() + 1, + otherProcessName + ) + whenever(mockActivityManager.runningAppProcesses) + .thenReturn(listOf(otherProcess)) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.isMainProcess).isTrue() + } + + @Test + fun `M build config W buildFilePersistenceConfig()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // When + val config = testedFeature.buildFilePersistenceConfig() + + // Then + assertThat(config.maxBatchSize) + .isEqualTo(FilePersistenceConfig.MAX_BATCH_SIZE) + assertThat(config.maxDiskSpace) + .isEqualTo(FilePersistenceConfig.MAX_DISK_SPACE) + assertThat(config.oldFileThreshold) + .isEqualTo(FilePersistenceConfig.OLD_FILE_THRESHOLD) + assertThat(config.maxItemsPerBatch) + .isEqualTo(FilePersistenceConfig.MAX_ITEMS_PER_BATCH) + assertThat(config.recentDelayMs) + .isEqualTo(fakeConfig.coreConfig.batchSize.windowDurationMs) + } + + @Test + fun `M initialize the NdkCrashHandler data W initialize() {main process}`( + @TempDir tempDir: File, + @StringForgery otherProcessName: String + ) { + // Given + val mockActivityManager = mock() + whenever(appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( + mockActivityManager + ) + val myProcess = forgeAppProcessInfo( + Process.myPid(), + appContext.fakePackageName + ) + val otherProcess = forgeAppProcessInfo( + Process.myPid() + 1, + otherProcessName + ) + whenever(mockActivityManager.runningAppProcesses) + .thenReturn(listOf(myProcess, otherProcess)) + whenever(appContext.mockInstance.cacheDir) doReturn tempDir + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.ndkCrashHandler) + .isInstanceOfSatisfying(DatadogNdkCrashHandler::class.java) { + assertThat(it.ndkCrashDataDirectory.parentFile).isEqualTo( + File( + tempDir, + CoreFeature.DATADOG_STORAGE_DIR_NAME.format(Locale.US, fakeSdkInstanceId) + ) + ) + } + } + + @Test + fun `M initialize the NdkCrashHandler data W initialize() {source type override}`( + @TempDir tempDir: File, + @StringForgery otherProcessName: String + ) { + // Given + fakeConfig = fakeConfig.copy( + additionalConfig = fakeConfig.additionalConfig.toMutableMap().apply { + put(Datadog.DD_NATIVE_SOURCE_TYPE, "ndk+il2cpp") + } + ) + val mockActivityManager = mock() + whenever(appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( + mockActivityManager + ) + val myProcess = forgeAppProcessInfo( + Process.myPid(), + appContext.fakePackageName + ) + val otherProcess = forgeAppProcessInfo( + Process.myPid() + 1, + otherProcessName + ) + whenever(mockActivityManager.runningAppProcesses) + .thenReturn(listOf(myProcess, otherProcess)) + whenever(appContext.mockInstance.cacheDir) doReturn tempDir + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.ndkCrashHandler) + .isInstanceOfSatisfying(DatadogNdkCrashHandler::class.java) { + assertThat(it.nativeCrashSourceType).isEqualTo("ndk+il2cpp") + } + } + + @Test + fun `M not initialize the NdkCrashHandler data W initialize() {not main process}`( + @StringForgery otherProcessName: String + ) { + // Given + val mockActivityManager = mock() + whenever(appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( + mockActivityManager + ) + val myProcess = forgeAppProcessInfo(Process.myPid(), otherProcessName) + whenever(mockActivityManager.runningAppProcesses) + .thenReturn(listOf(myProcess)) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.ndkCrashHandler).isInstanceOf(NoOpNdkCrashHandler::class.java) + } + + @Test + fun `M initialize storage directory W initialize()`( + @TempDir tempDir: File + ) { + // Given + whenever(appContext.mockInstance.cacheDir) doReturn tempDir + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.storageDir).isEqualTo( + File( + tempDir, + CoreFeature.DATADOG_STORAGE_DIR_NAME.format(Locale.US, fakeSdkInstanceId) + ) + ) + } + + @Test + fun `M initialise encryption W initialize`( + @IntForgery(-128, 128) fakeByte: Int + ) { + // Given + val mockEncryption = mock() + whenever(mockEncryption.encrypt(any())) doAnswer { invocation -> + (invocation.getArgument(0) as ByteArray) + .map { it xor fakeByte.toByte() } + .toByteArray() + } + fakeConfig = fakeConfig.copy( + coreConfig = fakeConfig.coreConfig.copy( + encryption = mockEncryption + ) + ) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.localDataEncryption).isNotNull() + .isSameAs(mockEncryption) + } + + @Test + fun `M initialise persistence strategy W initialize`() { + // Given + val mockPersistenceStrategyFactory = mock() + fakeConfig = fakeConfig.copy( + coreConfig = fakeConfig.coreConfig.copy( + persistenceStrategyFactory = mockPersistenceStrategyFactory + ) + ) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.persistenceStrategyFactory).isNotNull() + .isSameAs(mockPersistenceStrategyFactory) + } + + // endregion + + @Test + fun `M return last fatal ANR sent W lastFatalAnrSent`( + @TempDir tempDir: File, + @LongForgery(min = 0L) fakeLastFatalAnrSent: Long + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME) + .writeText(fakeLastFatalAnrSent.toString()) + + // When + val lastFatalAnrSent = testedFeature.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isEqualTo(fakeLastFatalAnrSent) + } + + @Test + fun `M return null W lastFatalAnrSent { no file }`( + @TempDir tempDir: File + ) { + // Given + testedFeature.storageDir = tempDir + + // When + val lastFatalAnrSent = testedFeature.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isNull() + } + + @Test + fun `M return null W lastFatalAnrSent { file contains not a number }`( + @TempDir tempDir: File, + @StringForgery fakeBrokenLastFatalAnrSent: String + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME) + .writeText(fakeBrokenLastFatalAnrSent) + + // When + val lastFatalAnrSent = testedFeature.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isNull() + } + + @Test + fun `M delete last fatal ANR sent W deleteLastFatalAnrSent`( + @TempDir tempDir: File, + @LongForgery fakeLastFatalAnrSent: Long + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME) + .writeText(fakeLastFatalAnrSent.toString()) + + // When + testedFeature.deleteLastFatalAnrSent() + + // Then + assertThat(File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME)).doesNotExist() + } + + @Test + fun `M write last view event W writeLastViewEvent`( + @TempDir tempDir: File, + @StringForgery viewEvent: String + ) { + // Given + val fakeViewEvent = viewEvent.toByteArray() + + testedFeature.storageDir = tempDir + + // When + testedFeature.writeLastViewEvent(fakeViewEvent) + + // Then + val lastViewEventFile = File( + tempDir, + CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME + ) + assertThat(lastViewEventFile).exists() + + val fileContent = lastViewEventFile.readBytes() + // file will have batch file format, so beginning will contain some metadata, + // we need to skip it for the comparison + val payload = fileContent.takeLast(fakeViewEvent.size).toByteArray() + assertThat(payload).isEqualTo(fakeViewEvent) + } + + @Test + fun `M delete last view event W deleteLastViewEvent`( + @TempDir tempDir: File, + @StringForgery fakeViewEvent: String + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME) + .writeText(fakeViewEvent) + + // When + testedFeature.deleteLastViewEvent() + + // Then + assertThat(File(tempDir, CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME)).doesNotExist() + } + + @Suppress("DEPRECATION") + @Test + fun `M delete last view event W deleteLastViewEvent { legacy NDK location }`( + @TempDir tempDir: File, + @StringForgery fakeViewEvent: String + ) { + // Given + testedFeature.storageDir = tempDir + DatadogNdkCrashHandler.getLastViewEventFile(tempDir) + .apply { + parentFile?.mkdirs() + } + .writeText(fakeViewEvent) + + // When + testedFeature.deleteLastViewEvent() + + // Then + assertThat(DatadogNdkCrashHandler.getLastViewEventFile(tempDir)).doesNotExist() + } + + @Test + fun `M return null W lastViewEvent { no last view event written }`( + @TempDir tempDir: File + ) { + // Given + testedFeature.storageDir = tempDir + + // When + val lastViewEvent = testedFeature.lastViewEvent + + // Then + assertThat(lastViewEvent).isNull() + } + + @Test + fun `M return last view event W lastViewEvent`( + @TempDir tempDir: File, + @Forgery fakeViewEvent: JsonObject + ) { + // Given + testedFeature.storageDir = tempDir + testedFeature.writeLastViewEvent(fakeViewEvent.toString().toByteArray()) + + // When + val lastViewEvent = testedFeature.lastViewEvent + + // Then + assertThat(lastViewEvent.toString()).isEqualTo(fakeViewEvent.toString()) + // file must be deleted once view event is read + assertThat(File(tempDir, CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME)).doesNotExist() + } + + @Test + fun `M return last view event W lastViewEvent { check old NDK location }`( + @TempDir tempDir: File, + @Forgery fakeViewEvent: JsonObject + ) { + // Given + testedFeature.storageDir = tempDir + + @Suppress("DEPRECATION") + val legacyNdkViewEventFile = DatadogNdkCrashHandler.getLastViewEventFile(tempDir) + legacyNdkViewEventFile.parentFile?.mkdirs() + + BatchFileReaderWriter + .create(internalLogger = mock(), encryption = null) + .writeData( + legacyNdkViewEventFile, + RawBatchEvent(fakeViewEvent.toString().toByteArray()), + append = false + ) + + // When + val lastViewEvent = testedFeature.lastViewEvent + + // Then + assertThat(lastViewEvent.toString()).isEqualTo(fakeViewEvent.toString()) + } + + @Test + fun `M return app start time W appStartTimeNs`( + @LongForgery(min = 0L) fakeAppStartTimeNs: Long + ) { + // Given + whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn fakeAppStartTimeNs + + // When + val appStartTimeNs = testedFeature.appStartTimeNs + + // Then + assertThat(appStartTimeNs).isEqualTo(fakeAppStartTimeNs) + } + + // region shutdown + + @Test + fun `M cleanup NdkCrashHandler W stop()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.ndkCrashHandler).isInstanceOf(NoOpNdkCrashHandler::class.java) + } + + @Test + fun `M cleanup app info W stop()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.clientToken).isEqualTo("") + assertThat(testedFeature.packageVersionProvider.version).isEqualTo("") + assertThat(testedFeature.serviceName).isEqualTo("") + assertThat(testedFeature.envName).isEqualTo("") + assertThat(testedFeature.variant).isEqualTo("") + assertThat(testedFeature.contextRef.get()).isNull() + } + + @Test + fun `M cleanup providers W stop()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.firstPartyHostHeaderTypeResolver.knownHosts) + .isEmpty() + assertThat(testedFeature.networkInfoProvider) + .isInstanceOf(NoOpNetworkInfoProvider::class.java) + assertThat(testedFeature.systemInfoProvider) + .isInstanceOf(NoOpSystemInfoProvider::class.java) + assertThat(testedFeature.timeProvider) + .isInstanceOf(DefaultTimeProvider::class.java) + assertThat(testedFeature.trackingConsentProvider) + .isInstanceOf(NoOpConsentProvider::class.java) + assertThat(testedFeature.userInfoProvider) + .isInstanceOf(NoOpMutableUserInfoProvider::class.java) + } + + @Test + fun `M shut down executors W stop()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + val mockUploadExecutorService = mock() + testedFeature.uploadExecutorService = mockUploadExecutorService + val mockPersistenceExecutorService = mock() + testedFeature.persistenceExecutorService = mockPersistenceExecutorService + val mockContextExecutorService = mock() + testedFeature.contextExecutorService = mockContextExecutorService + + // When + testedFeature.stop() + + // Then + verify(mockUploadExecutorService).shutdownNow() + verify(mockPersistenceExecutorService).shutdownNow() + verify(mockContextExecutorService).shutdownNow() + } + + @Test + fun `M unregister tracking consent callbacks W stop()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + val mockConsentProvider: ConsentProvider = mock() + testedFeature.trackingConsentProvider = mockConsentProvider + + // When + testedFeature.stop() + + // Then + verify(mockConsentProvider).unregisterAllCallbacks() + } + + @Test + fun `M drain the persistence executor queue W drainAndShutdownExecutors()`(forge: Forge) { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + val blockingQueue = LinkedBlockingQueue(forge.aList { mock() }) + val persistenceExecutor: FlushableExecutorService = mock() + whenever(persistenceExecutor.drainTo(any())) doAnswer { invocation -> + blockingQueue.forEach { + @Suppress("UNCHECKED_CAST") + (invocation.arguments[0] as MutableCollection).add(it) + } + } + testedFeature.persistenceExecutorService = persistenceExecutor + + // When + testedFeature.drainAndShutdownExecutors() + + // Then + blockingQueue.forEach { + verify(it).run() + } + } + + @Test + fun `M drain the upload executor queue W drainAndShutdownExecutors()`(forge: Forge) { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + val blockingQueue = LinkedBlockingQueue(forge.aList { mock() }) + val mockUploadExecutor: ScheduledThreadPoolExecutor = mock() + whenever(mockUploadExecutor.queue).thenReturn(blockingQueue) + testedFeature.uploadExecutorService = mockUploadExecutor + + // When + testedFeature.drainAndShutdownExecutors() + + // Then + blockingQueue.forEach { + verify(it).run() + } + } + + @Test + fun `M drain the context executor queue W drainAndShutdownExecutors()`(forge: Forge) { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + val blockingQueue = LinkedBlockingQueue(forge.aList { mock() }) + val mockContextExecutor = mock() + whenever(mockContextExecutor.queue).thenReturn(blockingQueue) + testedFeature.contextExecutorService = mockContextExecutor + + // When + testedFeature.drainAndShutdownExecutors() + + // Then + blockingQueue.forEach { + verify(it).run() + } + } + + @Test + fun `M shutdown with wait the persistence executor W drainAndShutdownExecutors()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + val mockPersistenceExecutorService: FlushableExecutorService = mock() + testedFeature.persistenceExecutorService = mockPersistenceExecutorService + + // When + testedFeature.drainAndShutdownExecutors() + + // Then + inOrder(mockPersistenceExecutorService) { + verify(mockPersistenceExecutorService).shutdown() + verify(mockPersistenceExecutorService).awaitTermination(10, TimeUnit.SECONDS) + } + } + + @Test + fun `M shutdown with wait the upload executor W drainAndShutdownExecutors()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + val blockingQueue = LinkedBlockingQueue() + val mockUploadService: ScheduledThreadPoolExecutor = mock() + whenever(mockUploadService.queue).thenReturn(blockingQueue) + testedFeature.uploadExecutorService = mockUploadService + + // When + testedFeature.drainAndShutdownExecutors() + + // Then + inOrder(mockUploadService) { + verify(mockUploadService).shutdown() + verify(mockUploadService).awaitTermination(10, TimeUnit.SECONDS) + } + } + + @Test + fun `M shutdown with wait the context executor W drainAndShutdownExecutors()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + val blockingQueue = LinkedBlockingQueue() + val mockContextExecutor = mock() + whenever(mockContextExecutor.queue).thenReturn(blockingQueue) + testedFeature.contextExecutorService = mockContextExecutor + + // When + testedFeature.drainAndShutdownExecutors() + + // Then + inOrder(mockContextExecutor) { + verify(mockContextExecutor).shutdown() + verify(mockContextExecutor).awaitTermination(10, TimeUnit.SECONDS) + } + } + + // endregion + + // region Internal + + private fun forgeAppProcessInfo( + processId: Int, + processName: String + ): ActivityManager.RunningAppProcessInfo { + return ActivityManager.RunningAppProcessInfo().apply { + this.processName = processName + this.pid = processId + } + } + + // endregion + + companion object { + val appContext = ApplicationContextTestConfiguration(Application::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/DatadogContextProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/DatadogContextProviderTest.kt new file mode 100644 index 0000000000..f579744d27 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/DatadogContextProviderTest.kt @@ -0,0 +1,205 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import android.app.Application +import com.datadog.android.api.context.AccountInfo +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.core.internal.system.AndroidInfoProvider +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.config.CoreFeatureTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogContextProviderTest { + + private lateinit var testedProvider: ContextProvider + + @Forgery + lateinit var fakeNetworkInfo: NetworkInfo + + @Forgery + lateinit var fakeUserInfo: UserInfo + + @Forgery + lateinit var fakeAccountInfo: AccountInfo + + @Forgery + lateinit var fakeAndroidInfo: AndroidInfoProvider + + @LongForgery(min = 0L) + var fakeDeviceTimestamp: Long = 0L + + @LongForgery(min = 0L) + var fakeServerTimestamp: Long = 0L + + @Forgery + lateinit var fakeTrackingConsent: TrackingConsent + + lateinit var fakeFeaturesContext: Map> + + @Mock + lateinit var mockFeatureContextProvider: FeatureContextProvider + + @BeforeEach + fun setUp(forge: Forge) { + testedProvider = DatadogContextProvider(coreFeature.mockInstance, mockFeatureContextProvider) + + whenever(coreFeature.mockInstance.userInfoProvider.getUserInfo()) doReturn fakeUserInfo + whenever(coreFeature.mockInstance.accountInfoProvider.getAccountInfo()) doReturn fakeAccountInfo + whenever( + coreFeature.mockInstance.networkInfoProvider.getLatestNetworkInfo() + ) doReturn fakeNetworkInfo + + whenever(coreFeature.mockInstance.androidInfoProvider) doReturn fakeAndroidInfo + + whenever(coreFeature.mockInstance.timeProvider.getDeviceTimestamp()) doReturn + fakeDeviceTimestamp + whenever(coreFeature.mockInstance.timeProvider.getServerTimestamp()) doReturn + fakeServerTimestamp + + whenever(coreFeature.mockInstance.trackingConsentProvider.getConsent()) doReturn + fakeTrackingConsent + // building nested maps with default size slows down tests quite a lot, so will use + // an explicit small size + fakeFeaturesContext = forge.aMap(size = 2) { + forge.anAlphabeticalString() to forge.exhaustiveAttributes() + } + whenever( + mockFeatureContextProvider.getFeatureContext(any()) + ) doAnswer { + val featureName = it.getArgument(0) + fakeFeaturesContext[featureName] + } + } + + @Test + fun `M create a context W getContext()`() { + // When + val context = testedProvider.getContext(fakeFeaturesContext.keys) + + // Then + assertThat(context.site).isEqualTo(coreFeature.mockInstance.site) + assertThat(context.env).isEqualTo(coreFeature.mockInstance.envName) + assertThat(context.clientToken).isEqualTo(coreFeature.mockInstance.clientToken) + assertThat(context.service).isEqualTo(coreFeature.mockInstance.serviceName) + assertThat(context.env).isEqualTo(coreFeature.mockInstance.envName) + assertThat(context.version) + .isEqualTo(coreFeature.mockInstance.packageVersionProvider.version) + assertThat(context.sdkVersion).isEqualTo(coreFeature.mockInstance.sdkVersion) + assertThat(context.source).isEqualTo(coreFeature.mockInstance.sourceName) + + // time info + assertThat(context.time.deviceTimeNs) + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(fakeDeviceTimestamp)) + assertThat(context.time.serverTimeNs) + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(fakeServerTimestamp)) + assertThat(context.time.serverTimeOffsetMs) + .isEqualTo(fakeServerTimestamp - fakeDeviceTimestamp) + assertThat(context.time.serverTimeOffsetNs) + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(fakeServerTimestamp - fakeDeviceTimestamp)) + + // process info + assertThat(context.processInfo.isMainProcess) + .isEqualTo(coreFeature.mockInstance.isMainProcess) + + // network info + assertThat(context.networkInfo.connectivity.name) + .isEqualTo(fakeNetworkInfo.connectivity.name) + assertThat(context.networkInfo.carrierName) + .isEqualTo(fakeNetworkInfo.carrierName) + assertThat(context.networkInfo.carrierId) + .isEqualTo(fakeNetworkInfo.carrierId) + assertThat(context.networkInfo.cellularTechnology) + .isEqualTo(fakeNetworkInfo.cellularTechnology) + assertThat(context.networkInfo.upKbps) + .isEqualTo(fakeNetworkInfo.upKbps) + assertThat(context.networkInfo.downKbps) + .isEqualTo(fakeNetworkInfo.downKbps) + assertThat(context.networkInfo.strength) + .isEqualTo(fakeNetworkInfo.strength) + + // device info + assertThat(context.deviceInfo.deviceBrand).isEqualTo(fakeAndroidInfo.deviceBrand) + assertThat(context.deviceInfo.deviceName).isEqualTo(fakeAndroidInfo.deviceName) + assertThat(context.deviceInfo.deviceType).isEqualTo(fakeAndroidInfo.deviceType) + assertThat(context.deviceInfo.deviceModel).isEqualTo(fakeAndroidInfo.deviceModel) + assertThat(context.deviceInfo.deviceBuildId).isEqualTo(fakeAndroidInfo.deviceBuildId) + assertThat(context.deviceInfo.osName).isEqualTo(fakeAndroidInfo.osName) + assertThat(context.deviceInfo.osVersion).isEqualTo(fakeAndroidInfo.osVersion) + assertThat(context.deviceInfo.osMajorVersion).isEqualTo(fakeAndroidInfo.osMajorVersion) + assertThat(context.deviceInfo.architecture).isEqualTo(fakeAndroidInfo.architecture) + assertThat(context.deviceInfo.numberOfDisplays).isEqualTo(fakeAndroidInfo.numberOfDisplays) + + // locale info + assertThat(context.deviceInfo.localeInfo.locales).isEqualTo(fakeAndroidInfo.locales) + assertThat(context.deviceInfo.localeInfo.currentLocale).isEqualTo(fakeAndroidInfo.currentLocale) + assertThat(context.deviceInfo.localeInfo.timeZone).isEqualTo(fakeAndroidInfo.timeZone) + + // user info + assertThat(context.userInfo.id).isEqualTo(fakeUserInfo.id) + assertThat(context.userInfo.name).isEqualTo(fakeUserInfo.name) + assertThat(context.userInfo.email).isEqualTo(fakeUserInfo.email) + assertThat(context.userInfo.additionalProperties) + .isEqualTo(fakeUserInfo.additionalProperties) + + // account info + assertThat(context.accountInfo).isNotNull + context.accountInfo?.let { + assertThat(it.id).isEqualTo(fakeAccountInfo.id) + assertThat(it.name).isEqualTo(fakeAccountInfo.name) + assertThat(it.extraInfo).isEqualTo(fakeAccountInfo.extraInfo) + } + + assertThat(context.appBuildId).isEqualTo(coreFeature.mockInstance.appBuildId) + assertThat(context.trackingConsent).isEqualTo(fakeTrackingConsent) + + assertThat(context.featuresContext).isEqualTo(fakeFeaturesContext) + } + + companion object { + val appContext = ApplicationContextTestConfiguration(Application::class.java) + val coreFeature = CoreFeatureTestConfiguration(appContext) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext, coreFeature) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkCoreRegistryTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkCoreRegistryTest.kt new file mode 100644 index 0000000000..0c198448b0 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkCoreRegistryTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class SdkCoreRegistryTest { + + lateinit var testedRegistry: SdkCoreRegistry + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockSdkCore: SdkCore + + @BeforeEach + fun setUp() { + testedRegistry = SdkCoreRegistry(mockInternalLogger) + } + + @Test + fun `M return sdkCore W register+getInstance`( + @StringForgery name: String + ) { + // When + testedRegistry.register(name, mockSdkCore) + val instance = testedRegistry.getInstance(name) + + // Then + assertThat(instance).isSameAs(mockSdkCore) + } + + @Test + fun `M return sdkCore W register+getInstance {default name}`() { + // When + testedRegistry.register(null, mockSdkCore) + val instance = testedRegistry.getInstance() + + // Then + assertThat(instance).isSameAs(mockSdkCore) + } + + @Test + fun `M return null W register+getInstance {different names}`( + @StringForgery name1: String, + @StringForgery name2: String + ) { + // When + testedRegistry.register(name1, mockSdkCore) + val instance = testedRegistry.getInstance(name2) + + // Then + assertThat(instance).isNull() + } + + @Test + fun `M return null W getInstance {nothing registered}`( + @StringForgery name: String + ) { + // When + val instance = testedRegistry.getInstance(name) + + // Then + assertThat(instance).isNull() + } + + @Test + fun `M return null W getInstance {no default registered}`() { + // When + val instance = testedRegistry.getInstance() + + // Then + assertThat(instance).isNull() + } + + @Test + fun `M warn and ignore W register {name already registered}`( + @StringForgery name: String + ) { + // Given + val otherMockSdkCore = mock() + + // When + testedRegistry.register(name, mockSdkCore) + testedRegistry.register(name, otherMockSdkCore) + val instance = testedRegistry.getInstance(name) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "An SdkCode with name $name has already been registered." + ) + assertThat(instance).isSameAs(mockSdkCore) + } + + @Test + fun `M return null W register+unregister+getInstance`( + @StringForgery name: String + ) { + // When + testedRegistry.register(name, mockSdkCore) + val unregistered = testedRegistry.unregister(name) + val instance = testedRegistry.getInstance(name) + + // Then + assertThat(unregistered).isSameAs(mockSdkCore) + assertThat(instance).isNull() + } + + @Test + fun `M return null W register+unregister+getInstance {default name name}`() { + // When + testedRegistry.register(null, mockSdkCore) + val unregistered = testedRegistry.unregister() + val instance = testedRegistry.getInstance() + + // Then + assertThat(unregistered).isSameAs(mockSdkCore) + assertThat(instance).isNull() + } + + @Test + fun `M clear registered instances W register+clear+getInstance`( + @StringForgery name: String + ) { + // When + testedRegistry.register(name, mockSdkCore) + testedRegistry.clear() + val instance = testedRegistry.getInstance() + + // Then + assertThat(instance).isNull() + } + + @Test + fun `M clear registered instances W register+clear+getInstance {default name name}`() { + // When + testedRegistry.register(null, mockSdkCore) + testedRegistry.clear() + val instance = testedRegistry.getInstance() + + // Then + assertThat(instance).isNull() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt new file mode 100644 index 0000000000..5bccc7d986 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt @@ -0,0 +1,829 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import android.app.Application +import android.content.Context +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureEventReceiver +import com.datadog.android.api.feature.StorageBackedFeature +import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.core.configuration.BatchProcessingLevel +import com.datadog.android.core.configuration.BatchSize +import com.datadog.android.core.configuration.UploadFrequency +import com.datadog.android.core.internal.SdkFeature.Companion.BATCH_COUNT_METRIC_NAME +import com.datadog.android.core.internal.SdkFeature.Companion.METER_NAME +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import com.datadog.android.core.internal.data.upload.DataOkHttpUploader +import com.datadog.android.core.internal.data.upload.DataUploadRunnable +import com.datadog.android.core.internal.data.upload.DataUploadScheduler +import com.datadog.android.core.internal.data.upload.DefaultUploadSchedulerStrategy +import com.datadog.android.core.internal.data.upload.NoOpDataUploader +import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler +import com.datadog.android.core.internal.data.upload.UploadScheduler +import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor +import com.datadog.android.core.internal.metrics.BatchMetricsDispatcher +import com.datadog.android.core.internal.metrics.BatchMetricsDispatcher.Companion.TRACK_KEY +import com.datadog.android.core.internal.metrics.NoOpMetricsDispatcher +import com.datadog.android.core.internal.persistence.AbstractStorage +import com.datadog.android.core.internal.persistence.ConsentAwareStorage +import com.datadog.android.core.internal.persistence.NoOpStorage +import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator +import com.datadog.android.core.internal.persistence.file.batch.BatchFileOrchestrator +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.internal.profiler.BenchmarkMeter +import com.datadog.android.internal.profiler.BenchmarkSdkUploads +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.privacy.TrackingConsentProviderCallback +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.config.CoreFeatureTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Locale +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.atomic.AtomicBoolean + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class SdkFeatureTest { + + private lateinit var testedFeature: SdkFeature + + @Mock + lateinit var mockStorage: Storage + + @Mock + lateinit var mockDataStore: DataStoreHandler + + @Mock + lateinit var mockWrappedFeature: StorageBackedFeature + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockBenchmarkSdkUploads: BenchmarkSdkUploads + + @Mock + lateinit var mockContextProvider: ContextProvider + + @Forgery + lateinit var fakeConsent: TrackingConsent + + @Forgery + lateinit var fakeStorageConfiguration: FeatureStorageConfiguration + + @StringForgery + lateinit var fakeInstanceId: String + + @StringForgery + lateinit var fakeFeatureName: String + + private lateinit var fakeCoreUploadFrequency: UploadFrequency + + private lateinit var fakeCoreBatchSize: BatchSize + + private lateinit var fakeCoreBatchProcessingLevel: BatchProcessingLevel + + @BeforeEach + fun `set up`(forge: Forge) { + fakeCoreUploadFrequency = forge.aValueFrom(UploadFrequency::class.java) + fakeCoreBatchSize = forge.aValueFrom(BatchSize::class.java) + fakeCoreBatchProcessingLevel = forge.aValueFrom(BatchProcessingLevel::class.java) + whenever(coreFeature.mockInstance.batchSize).thenReturn(fakeCoreBatchSize) + whenever(coreFeature.mockInstance.uploadFrequency).thenReturn(fakeCoreUploadFrequency) + whenever(coreFeature.mockInstance.batchProcessingLevel) + .thenReturn(fakeCoreBatchProcessingLevel) + whenever(coreFeature.mockTrackingConsentProvider.getConsent()) doReturn fakeConsent + whenever(coreFeature.mockInstance.initialized) doReturn AtomicBoolean(true) + whenever(mockWrappedFeature.name) doReturn fakeFeatureName + whenever(mockWrappedFeature.requestFactory) doReturn mock() + whenever(mockWrappedFeature.storageConfiguration) doReturn fakeStorageConfiguration + whenever(coreFeature.mockContextExecutorService.execute(any())) doAnswer { + it.getArgument(0).run() + } + testedFeature = SdkFeature( + coreFeature = coreFeature.mockInstance, + contextProvider = mockContextProvider, + wrappedFeature = mockWrappedFeature, + internalLogger = mockInternalLogger + ) + } + + @Test + fun `M mark itself as initialized W initialize()`() { + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.isInitialized()).isTrue() + } + + @Test + fun `M register ProcessLifecycleMonitor for MetricsDispatcher W initialize()`() { + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + argumentCaptor { + verify((appContext.mockInstance)).registerActivityLifecycleCallbacks(capture()) + assertThat(firstValue).isInstanceOf(ProcessLifecycleMonitor::class.java) + assertThat((firstValue as ProcessLifecycleMonitor).callback) + .isInstanceOf(BatchMetricsDispatcher::class.java) + } + } + + @Test + fun `M not throw W initialize(){ no app context }`() { + // When + assertDoesNotThrow { + testedFeature.initialize(mock(), fakeInstanceId) + } + } + + @Test + fun `M initialize uploader W initialize()`() { + // Given + val expectedUploadConfiguration = DataUploadConfiguration( + fakeCoreUploadFrequency, + fakeCoreBatchProcessingLevel.maxBatchesPerUploadJob + ) + + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.uploadScheduler).isInstanceOf(DataUploadScheduler::class.java) + val dataUploadRunnable = (testedFeature.uploadScheduler as DataUploadScheduler).runnable + val uploadSchedulerStrategy = (dataUploadRunnable.uploadSchedulerStrategy as? DefaultUploadSchedulerStrategy) + assertThat(uploadSchedulerStrategy?.uploadConfiguration).isEqualTo(expectedUploadConfiguration) + assertThat(dataUploadRunnable.maxBatchesPerJob).isEqualTo(fakeCoreBatchProcessingLevel.maxBatchesPerUploadJob) + argumentCaptor { + verify(coreFeature.mockUploadExecutor).execute( + argThat { this is DataUploadRunnable } + ) + } + assertThat(testedFeature.uploader).isInstanceOf(DataOkHttpUploader::class.java) + } + + @Test + fun `M initialize the storage W initialize()`() { + // Given + val fakeCorePersistenceConfig = FilePersistenceConfig() + whenever(coreFeature.mockInstance.buildFilePersistenceConfig()) + .thenReturn(fakeCorePersistenceConfig) + + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.storage).isInstanceOf(ConsentAwareStorage::class.java) + val consentAwareStorage = testedFeature.storage as ConsentAwareStorage + assertThat(consentAwareStorage.filePersistenceConfig.recentDelayMs) + .isEqualTo(fakeCoreBatchSize.windowDurationMs) + val pendingFileOrchestrator = + consentAwareStorage.pendingOrchestrator as BatchFileOrchestrator + val grantedFileOrchestrator = + consentAwareStorage.grantedOrchestrator as BatchFileOrchestrator + val expectedFilePersistenceConfig = fakeCorePersistenceConfig.copy( + maxBatchSize = fakeStorageConfiguration.maxBatchSize, + maxItemSize = fakeStorageConfiguration.maxItemSize, + maxItemsPerBatch = fakeStorageConfiguration.maxItemsPerBatch, + oldFileThreshold = fakeStorageConfiguration.oldBatchThreshold, + recentDelayMs = fakeCoreBatchSize.windowDurationMs + ) + assertThat(pendingFileOrchestrator.config).isEqualTo(expectedFilePersistenceConfig) + assertThat(grantedFileOrchestrator.config).isEqualTo(expectedFilePersistenceConfig) + } + + @Test + fun `M initialize the storage W initialize() {custom persistence strategy}`() { + // Given + val mockPersistenceStrategy = mock() + whenever(coreFeature.mockInstance.persistenceStrategyFactory) doReturn mockPersistenceStrategy + + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.storage).isInstanceOf(AbstractStorage::class.java) + val abstractStorage = testedFeature.storage as AbstractStorage + assertThat(abstractStorage.sdkCoreId).isEqualTo(fakeInstanceId) + assertThat(abstractStorage.persistenceStrategyFactory) + .isEqualTo(mockPersistenceStrategy) + } + + @Test + fun `M register tracking consent callback W initialize(){feature+TrackingConsentProviderCallback}`() { + // Given + val mockFeature = mock() + whenever(mockFeature.name).thenReturn(fakeFeatureName) + testedFeature = SdkFeature( + coreFeature.mockInstance, + mockContextProvider, + mockFeature, + internalLogger = mockInternalLogger + ) + + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + verify(coreFeature.mockInstance.trackingConsentProvider) + .registerCallback(mockFeature) + } + + @Test + fun `M not initialize storage and uploader W initialize() { simple feature }`() { + // Given + val mockSimpleFeature = mock().apply { + whenever(name) doReturn fakeFeatureName + } + testedFeature = SdkFeature( + coreFeature = coreFeature.mockInstance, + mockContextProvider, + wrappedFeature = mockSimpleFeature, + internalLogger = mockInternalLogger + ) + + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.isInitialized()).isTrue + + assertThat(testedFeature.uploadScheduler) + .isInstanceOf(NoOpUploadScheduler::class.java) + assertThat(testedFeature.uploader) + .isInstanceOf(NoOpDataUploader::class.java) + assertThat(testedFeature.storage) + .isInstanceOf(NoOpStorage::class.java) + assertThat(testedFeature.fileOrchestrator) + .isInstanceOf(NoOpFileOrchestrator::class.java) + } + + @Test + fun `M stop scheduler W stop()`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + val mockUploadScheduler: UploadScheduler = mock() + testedFeature.uploadScheduler = mockUploadScheduler + + // When + testedFeature.stop() + + // Then + verify(mockUploadScheduler).stopScheduling() + } + + @Test + fun `M unregister ProcessLifecycleMonitor W stop()`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // When + testedFeature.stop() + + // Then + verify(appContext.mockInstance).unregisterActivityLifecycleCallbacks( + argThat { this is ProcessLifecycleMonitor } + ) + } + + @Test + fun `M cleanup data W stop()`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.uploadScheduler) + .isInstanceOf(NoOpUploadScheduler::class.java) + assertThat(testedFeature.storage) + .isInstanceOf(NoOpStorage::class.java) + assertThat(testedFeature.uploader) + .isInstanceOf(NoOpDataUploader::class.java) + assertThat(testedFeature.fileOrchestrator) + .isInstanceOf(NoOpFileOrchestrator::class.java) + assertThat(testedFeature.processLifecycleMonitor).isNull() + assertThat(testedFeature.metricsDispatcher).isInstanceOf(NoOpMetricsDispatcher::class.java) + assertThat(testedFeature.featureContext).isEmpty() + } + + @Test + fun `M mark itself as not initialized W stop()`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.isInitialized()).isFalse() + } + + @Test + fun `M call wrapped feature onStop W stop()`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // When + testedFeature.stop() + + // Then + verify(mockWrappedFeature).onStop() + } + + @Test + fun `M unregister tracking consent callback W stop(){feature+TrackingConsentProviderCallback}`() { + // Given + val mockFeature = mock().apply { + whenever(name) doReturn fakeFeatureName + } + testedFeature = SdkFeature( + coreFeature = coreFeature.mockInstance, + contextProvider = mockContextProvider, + wrappedFeature = mockFeature, + internalLogger = mockInternalLogger + ) + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // When + testedFeature.stop() + + // Then + verify(coreFeature.mockInstance.trackingConsentProvider).unregisterCallback(mockFeature) + } + + @Test + fun `M initialize only once W initialize() twice`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + val uploadScheduler = testedFeature.uploadScheduler + val uploader = testedFeature.uploader + val storage = testedFeature.storage + val fileOrchestrator = testedFeature.fileOrchestrator + + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.uploadScheduler).isSameAs(uploadScheduler) + assertThat(testedFeature.uploader).isSameAs(uploader) + assertThat(testedFeature.storage).isSameAs(storage) + assertThat(testedFeature.fileOrchestrator).isSameAs(fileOrchestrator) + } + + @Test + fun `M not setup uploader W initialize() in secondary process`() { + // Given + whenever(testedFeature.coreFeature.isMainProcess) doReturn false + + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.uploadScheduler).isInstanceOf(NoOpUploadScheduler::class.java) + } + + @Test + fun `M clear local storage W clearAllData()`() { + // Given + testedFeature.storage = mockStorage + + // When + testedFeature.clearAllData() + + // Then + verify(mockStorage).dropAll() + } + + @Test + fun `M clear data store W clearAllData()`() { + // Given + testedFeature.dataStore = mockDataStore + + // When + testedFeature.clearAllData() + + // Then + verify(mockDataStore).clearAllData() + } + + // region FeatureScope + + @Test + fun `M unregister datastore W stop()`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + assertThat(testedFeature.dataStore) + .isNotInstanceOf(NoOpDataStoreHandler::class.java) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.dataStore).isInstanceOf(NoOpDataStoreHandler::class.java) + } + + @Test + fun `M register datastore W initialize()`() { + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.dataStore) + .isNotInstanceOf(NoOpDataStoreHandler::class.java) + } + + @Test + fun `M provide write context W withWriteContext(callback)`( + @Forgery fakeContext: DatadogContext, + @StringForgery fakeWithFeatureContexts: Set, + @Mock mockEventWriteScope: EventWriteScope + ) { + // Given + testedFeature.storage = mockStorage + val callback = mock<(DatadogContext, EventWriteScope) -> Unit>() + whenever(mockContextProvider.getContext(fakeWithFeatureContexts)) doReturn fakeContext + + whenever( + mockStorage.getEventWriteScope(fakeContext) + ) doReturn mockEventWriteScope + + // When + testedFeature.withWriteContext(fakeWithFeatureContexts, callback = callback) + + // Then + verify(callback).invoke( + fakeContext, + mockEventWriteScope + ) + } + + @Test + fun `M not provide write context W withWriteContext(callback) { CoreFeature is not initialized }`( + @StringForgery fakeWithFeatureContexts: Set + ) { + // Given + testedFeature.storage = mockStorage + val callback = mock<(DatadogContext, EventWriteScope) -> Unit>() + whenever(coreFeature.mockInstance.initialized) doReturn AtomicBoolean(false) + + // When + testedFeature.withWriteContext(fakeWithFeatureContexts, callback = callback) + + // Then + verifyNoInteractions(callback, mockContextProvider, mockStorage) + } + + @Test + fun `M provide Datadog context W withContext(callback)`( + @Forgery fakeContext: DatadogContext, + @StringForgery fakeWithFeatureContexts: Set + ) { + // Given + testedFeature.storage = mockStorage + val callback = mock<(DatadogContext) -> Unit>() + whenever(mockContextProvider.getContext(fakeWithFeatureContexts)) doReturn fakeContext + + // When + testedFeature.withContext(fakeWithFeatureContexts, callback = callback) + + // Then + verify(callback).invoke(fakeContext) + } + + @Test + fun `M not provide Datadog context W withContext(callback) { CoreFeature is not initialized }`( + @StringForgery fakeWithFeatureContexts: Set + ) { + // Given + testedFeature.storage = mockStorage + val callback = mock<(DatadogContext) -> Unit>() + whenever(coreFeature.mockInstance.initialized) doReturn AtomicBoolean(false) + + // When + testedFeature.withContext(fakeWithFeatureContexts, callback = callback) + + // Then + verifyNoInteractions(callback, mockContextProvider) + } + + @Test + fun `M provide write context W getWriteContextSync()`( + @Forgery fakeContext: DatadogContext, + @StringForgery fakeWithFeatureContexts: Set, + @Mock mockEventWriteScope: EventWriteScope + ) { + // Given + testedFeature.storage = mockStorage + whenever(mockContextProvider.getContext(fakeWithFeatureContexts)) doReturn fakeContext + whenever(coreFeature.mockInstance.contextExecutorService.submit(any>())) doAnswer { + val callable = it.getArgument>>(0) + mock>().apply { + whenever(get()) doAnswer { callable.call() } + } + } + + whenever( + mockStorage.getEventWriteScope(fakeContext) + ) doReturn mockEventWriteScope + + // When + val writeContext = testedFeature.getWriteContextSync(fakeWithFeatureContexts) + + // Then + checkNotNull(writeContext) + assertThat(writeContext.first).isEqualTo(fakeContext) + assertThat(writeContext.second).isEqualTo(mockEventWriteScope) + } + + @Test + fun `M provide null write context W getWriteContextSync() { task rejected }`( + @Forgery fakeContext: DatadogContext, + @Mock mockEventWriteScope: EventWriteScope + ) { + // Given + testedFeature.storage = mockStorage + whenever( + coreFeature.mockInstance.contextExecutorService.submit(any>()) + ) doThrow RejectedExecutionException() + + whenever( + mockStorage.getEventWriteScope(fakeContext) + ) doReturn mockEventWriteScope + + // When + val writeContext = testedFeature.getWriteContextSync() + + // Then + assertThat(writeContext).isNull() + } + + @Test + fun `M provide null write context W getWriteContextSync() { failed to get task result }`( + @Forgery fakeContext: DatadogContext, + @Mock mockEventWriteScope: EventWriteScope, + forge: Forge + ) { + // Given + testedFeature.storage = mockStorage + val throwable = forge.anElementFrom( + CancellationException(), + ExecutionException(forge.aThrowable()), + InterruptedException() + ) + whenever(coreFeature.mockInstance.contextExecutorService.submit(any>())) doAnswer { + mock>().apply { + whenever(get()) doThrow throwable + } + } + + whenever( + mockStorage.getEventWriteScope(fakeContext) + ) doReturn mockEventWriteScope + + // When + val writeContext = testedFeature.getWriteContextSync() + + // Then + assertThat(writeContext).isNull() + } + + @Test + fun `M provide null write context W getWriteContextSync() { CoreFeature is not initialized }`() { + // Given + whenever(coreFeature.mockInstance.initialized) doReturn AtomicBoolean(false) + + // When + val writeContext = testedFeature.getWriteContextSync() + + // Then + assertThat(writeContext).isNull() + verifyNoInteractions(mockContextProvider, mockStorage) + } + + @Test + fun `M send event W sendEvent(event)`() { + // Given + val mockEventReceiver = mock() + testedFeature.eventReceiver.set(mockEventReceiver) + val fakeEvent = Any() + + // When + testedFeature.sendEvent(fakeEvent) + + // Then + verify(mockEventReceiver).onReceive(fakeEvent) + } + + @Test + fun `M notify no receiver W sendEvent(event)`() { + // Given + val fakeEvent = Any() + + // When + testedFeature.sendEvent(fakeEvent) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + SdkFeature.NO_EVENT_RECEIVER.format(Locale.US, fakeFeatureName) + ) + } + + @Test + fun `M give wrapped feature W unwrap()`( + @StringForgery fakeFeatureName: String + ) { + // Given + val fakeFeature = FakeFeature(fakeFeatureName) + + testedFeature = SdkFeature( + coreFeature = coreFeature.mockInstance, + contextProvider = mockContextProvider, + wrappedFeature = fakeFeature, + internalLogger = mockInternalLogger + ) + + // When + val unwrappedFeature = testedFeature.unwrap() + + // Then + assertThat(unwrappedFeature).isSameAs(fakeFeature) + } + + @Test + fun `M throw exception W unwrap() { wrong class }`( + @StringForgery fakeFeatureName: String + ) { + // Given + val fakeFeature = FakeFeature(fakeFeatureName) + + testedFeature = SdkFeature( + coreFeature = coreFeature.mockInstance, + contextProvider = mockContextProvider, + wrappedFeature = fakeFeature, + internalLogger = mockInternalLogger + ) + + // When + Then + assertThrows { + // strange enough nothing is thrown if we don't save the result. + // Kotlin compiler removing/optimizing code unused? + @Suppress("UNUSED_VARIABLE") + val result = testedFeature.unwrap() + } + } + + // endregion + + // region Feature fakes + + class FakeFeature(override val name: String) : Feature { + + override fun onInitialize(appContext: Context) { + // no-op + } + + override fun onStop() { + // no-op + } + } + + class AnotherFakeFeature(override val name: String) : Feature { + + override fun onInitialize(appContext: Context) { + // no-op + } + + override fun onStop() { + // no-op + } + } + + class TrackingConsentFeature(override val name: String) : + Feature, + TrackingConsentProviderCallback { + + override fun onInitialize(appContext: Context) { + // no-op + } + + override fun onStop() { + // no-op + } + + override fun onConsentUpdated( + previousConsent: TrackingConsent, + newConsent: TrackingConsent + ) { + // no-op + } + } + + // endregion + + // region Batch Count Benchmark + + @Test + fun `M send batch count benchmark metrics W initialize`( + @Mock mockContext: Context + ) { + // Given + val mockMeter: BenchmarkMeter = mock() + whenever(mockBenchmarkSdkUploads.getMeter(METER_NAME)) + .thenReturn(mockMeter) + + testedFeature = SdkFeature( + coreFeature = coreFeature.mockInstance, + contextProvider = mockContextProvider, + wrappedFeature = mockWrappedFeature, + internalLogger = mockInternalLogger, + benchmarkSdkUploads = mockBenchmarkSdkUploads + ) + + // When + testedFeature.initialize(mockContext, fakeInstanceId) + + // Then + verify( + mockBenchmarkSdkUploads + .getMeter(METER_NAME) + ) + .createObservableGauge( + metricName = eq(BATCH_COUNT_METRIC_NAME), + tags = eq(mapOf(TRACK_KEY to fakeFeatureName)), + callback = any() + ) + } + + // endregion + + companion object { + val appContext = ApplicationContextTestConfiguration(Application::class.java) + val coreFeature = CoreFeatureTestConfiguration(appContext) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext, coreFeature) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/Sha256HashGeneratorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/Sha256HashGeneratorTest.kt new file mode 100644 index 0000000000..70ef1dc432 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/Sha256HashGeneratorTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class Sha256HashGeneratorTest { + + private val testedGenerator = Sha256HashGenerator() + + @Test + fun `M generate hash of expected format W generate()`( + @StringForgery fakeInput: String + ) { + // When + val hash = testedGenerator.generate(fakeInput) + + // Then + assertThat(hash).isNotNull() + assertThat(hash).matches("[0-9a-z]+") + } + + @Test + fun `M generate same hash W generate() { same input }`( + @StringForgery fakeInput: String + ) { + // When + val hash1 = testedGenerator.generate(fakeInput) + val hash2 = testedGenerator.generate(fakeInput) + + // Then + assertThat(hash1).isEqualTo(hash2) + } + + @Test + fun `M generate different hash W generate() { different input }`( + @StringForgery fakeInput1: String, + @StringForgery fakeInput2: String + ) { + // When + val hash1 = testedGenerator.generate(fakeInput1) + val hash2 = testedGenerator.generate(fakeInput2) + + // Then + assertThat(hash1).isNotEqualTo(hash2) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/account/DatadogAccountInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/account/DatadogAccountInfoProviderTest.kt new file mode 100644 index 0000000000..3969b3ed35 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/account/DatadogAccountInfoProviderTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.account + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.AccountInfo +import com.datadog.android.core.internal.account.DatadogAccountInfoProvider.Companion.MSG_ACCOUNT_NULL +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.MapForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.verify +import org.mockito.quality.Strictness + +@ExtendWith(ForgeExtension::class) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class DatadogAccountInfoProviderTest { + + private lateinit var testedProvider: DatadogAccountInfoProvider + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedProvider = DatadogAccountInfoProvider(mockInternalLogger) + } + + @Test + fun `M return null W getAccountInfo() {account info not set}`() { + // When + val result = testedProvider.getAccountInfo() + + // Then + assertThat(result).isEqualTo(null) + } + + @Test + fun `M return saved accountInfo W setAccountInfo() and getAccountInfo()`( + @Forgery accountInfo: AccountInfo + ) { + // When + testedProvider.setAccountInfo( + accountInfo.id, + accountInfo.name, + accountInfo.extraInfo + ) + val result = testedProvider.getAccountInfo() + + // Then + assertThat(result).isEqualTo(accountInfo) + } + + @Test + fun `M keep existing properties W addExtraInfo() is called`( + @Forgery accountInfo: AccountInfo, + @StringForgery forge: Forge + ) { + // Given + val fakeExtraInfo = forge.exhaustiveAttributes() + testedProvider.setAccountInfo( + accountInfo.id, + accountInfo.name, + fakeExtraInfo + ) + + // When + testedProvider.addExtraInfo(fakeExtraInfo) + // Then + assertThat(testedProvider.getAccountInfo()?.extraInfo).isEqualTo(fakeExtraInfo) + } + + @Test + fun `M use immutable properties W addExtraInfo() is called { changing properties values }`( + @StringForgery forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String + ) { + // Given + val fakeProperties = forge.exhaustiveAttributes() + val fakeExpectedProperties = fakeProperties.toMap() + val fakeMutableProperties = fakeProperties.toMutableMap() + testedProvider.setAccountInfo(id, name, emptyMap()) + testedProvider.addExtraInfo(fakeMutableProperties) + + // When + fakeMutableProperties.keys.forEach { + fakeMutableProperties[it] = forge.anAlphabeticalString() + } + + // Then + assertThat( + testedProvider.getAccountInfo()?.extraInfo + ).isEqualTo(fakeExpectedProperties) + } + + @Test + fun `M use immutable properties W addExtraInfo() is called { adding properties }`( + @StringForgery forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String + ) { + // Given + val fakeProperties = forge.exhaustiveAttributes() + val fakeExpectedProperties = fakeProperties.toMap() + val fakeMutableProperties = fakeProperties.toMutableMap() + testedProvider.setAccountInfo(id, name, emptyMap()) + testedProvider.addExtraInfo(fakeMutableProperties) + + // When + repeat(forge.anInt(1, 10)) { + fakeMutableProperties[forge.anAlphabeticalString()] = forge.anAlphabeticalString() + } + + // Then + assertThat( + testedProvider.getAccountInfo()?.extraInfo + ).isEqualTo(fakeExpectedProperties) + } + + @Test + fun `M use immutable properties W addExtraInfo() is called { removing properties }`( + @StringForgery forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String + ) { + // Given + val fakeProperties = forge.exhaustiveAttributes() + val fakeExpectedProperties = fakeProperties.toMap() + val fakeMutableProperties = fakeProperties.toMutableMap() + testedProvider.setAccountInfo(id, name, emptyMap()) + testedProvider.addExtraInfo(fakeMutableProperties) + + // When + repeat(forge.anInt(1, fakeMutableProperties.size + 1)) { + fakeMutableProperties.remove(fakeMutableProperties.keys.random()) + } + + // Then + assertThat(testedProvider.getAccountInfo()?.extraInfo).isEqualTo(fakeExpectedProperties) + } + + @Test + fun `M keep new property key W addExtraInfo() is called and the key already exists`( + @Forgery accountInfo: AccountInfo, + @StringForgery key: String, + @StringForgery value1: String, + @StringForgery value2: String + ) { + // Given + testedProvider.setAccountInfo( + accountInfo.id, + accountInfo.name, + mutableMapOf(key to value1) + ) + + // When + testedProvider.addExtraInfo(mapOf(key to value2)) + + // Then + assertThat( + testedProvider.getAccountInfo()?.extraInfo + ).isEqualTo( + mapOf(key to value2) + ) + } + + @Test + fun `M use immutable values W setAccountInfo { changing properties values }()`( + forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeExtraInfo: Map + ) { + // Given + val fakeMutableExtraInfo = fakeExtraInfo.toMutableMap() + val fakeExpectedExtraInfo = fakeExtraInfo.toMap() + testedProvider.setAccountInfo(id, name, fakeMutableExtraInfo) + + // When + fakeMutableExtraInfo.keys.forEach { key -> + fakeMutableExtraInfo[key] = forge.anAlphaNumericalString() + } + + // Then + assertThat(testedProvider.getAccountInfo()?.extraInfo).isEqualTo( + fakeExpectedExtraInfo + ) + } + + @Test + fun `M use immutable values W setAccountInfo { adding properties }()`( + forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeExtraInfo: Map + ) { + // Given + val fakeMutableExtraInfo = fakeExtraInfo.toMutableMap() + val fakeExpectedExtraInfo = fakeExtraInfo.toMap() + testedProvider.setAccountInfo(id, name, fakeMutableExtraInfo) + + // When + repeat(forge.anInt(1, 10)) { + fakeMutableExtraInfo[forge.anAlphabeticalString()] = forge.anAlphabeticalString() + } + + // Then + assertThat(testedProvider.getAccountInfo()?.extraInfo).isEqualTo( + fakeExpectedExtraInfo + ) + } + + @Test + fun `M use immutable values W setAccountInfo { removing properties }()`( + forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeExtraInfo: Map + ) { + // Given + val fakeMutableExtraInfo = fakeExtraInfo.toMutableMap() + val fakeExpectedExtraInfo = fakeExtraInfo.toMap() + testedProvider.setAccountInfo(id, name, fakeMutableExtraInfo) + + // When + repeat(forge.anInt(1, fakeMutableExtraInfo.size + 1)) { + fakeMutableExtraInfo.remove(fakeMutableExtraInfo.keys.random()) + } + + // Then + assertThat(testedProvider.getAccountInfo()?.extraInfo).isEqualTo( + fakeExpectedExtraInfo + ) + } + + @Test + fun `M warn user with log W addExtraInfo { account info null }()`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeExtraInfo: Map + ) { + // Given + val fakeMutableExtraInfo = fakeExtraInfo.toMutableMap() + + // When + testedProvider.addExtraInfo(fakeMutableExtraInfo) + + // Then + assertThat(testedProvider.getAccountInfo()?.extraInfo).isEqualTo( + null + ) + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + level = eq(InternalLogger.Level.WARN), + target = eq(InternalLogger.Target.USER), + messageBuilder = capture(), + throwable = isNull(), + onlyOnce = any(), + additionalProperties = isNull() + ) + allValues.forEach { + assertThat(it()).isEqualTo(MSG_ACCOUNT_NULL) + } + } + } + + @Test + fun `M return null W call clearAccountInfo`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeExtraInfo: Map + ) { + // Given + val fakeMutableExtraInfo = fakeExtraInfo.toMutableMap() + testedProvider.setAccountInfo(id, name, fakeMutableExtraInfo) + + // When + testedProvider.clearAccountInfo() + + // Then + assertThat(testedProvider.getAccountInfo()).isNull() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/configuration/DataUploadConfigurationTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/configuration/DataUploadConfigurationTest.kt new file mode 100644 index 0000000000..d523ddff69 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/configuration/DataUploadConfigurationTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.configuration + +import com.datadog.android.core.configuration.UploadFrequency +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataUploadConfigurationTest { + + @Forgery + lateinit var fakeUploadFrequency: UploadFrequency + + lateinit var testedConfiguration: DataUploadConfiguration + + @BeforeEach + fun `set up`(forge: Forge) { + testedConfiguration = DataUploadConfiguration( + fakeUploadFrequency, + forge.anInt() + ) + } + + @Test + fun `M correctly compute the min delay`() { + assertThat(testedConfiguration.minDelayMs) + .isEqualTo(fakeUploadFrequency.baseStepMs * DataUploadConfiguration.MIN_DELAY_FACTOR) + } + + @Test + fun `M correctly compute the max delay`() { + assertThat(testedConfiguration.maxDelayMs) + .isEqualTo(fakeUploadFrequency.baseStepMs * DataUploadConfiguration.MAX_DELAY_FACTOR) + } + + @Test + fun `M correctly compute the default delay`() { + assertThat(testedConfiguration.defaultDelayMs) + .isEqualTo( + fakeUploadFrequency.baseStepMs * + DataUploadConfiguration.DEFAULT_DELAY_FACTOR + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/privacy/TrackingConsentProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/privacy/TrackingConsentProviderTest.kt new file mode 100644 index 0000000000..1b564a1090 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/privacy/TrackingConsentProviderTest.kt @@ -0,0 +1,215 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.privacy + +import com.datadog.android.core.internal.privacy.TrackingConsentProvider +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.privacy.TrackingConsentProviderCallback +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argForWhich +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class TrackingConsentProviderTest { + + lateinit var testedConsentProvider: TrackingConsentProvider + + @Mock + lateinit var mockedCallback: TrackingConsentProviderCallback + + @BeforeEach + fun `set up`() { + testedConsentProvider = TrackingConsentProvider(TrackingConsent.PENDING) + } + + @Test + fun `M hold PENDING consent by default W initialised`() { + assertThat(testedConsentProvider.getConsent()).isEqualTo(TrackingConsent.PENDING) + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class) + fun `M update last consent W required`(fakeConsent: TrackingConsent) { + // WHEN + testedConsentProvider.setConsent(fakeConsent) + + // THEN + assertThat(testedConsentProvider.getConsent()).isEqualTo(fakeConsent) + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class, names = ["PENDING"], mode = EnumSource.Mode.EXCLUDE) + fun `M notify callbacks W updating consent`(fakeConsent: TrackingConsent) { + // GIVEN + testedConsentProvider.registerCallback(mockedCallback) + + // WHEN + testedConsentProvider.setConsent(fakeConsent) + + // THEN + verify(mockedCallback).onConsentUpdated(TrackingConsent.PENDING, fakeConsent) + verifyNoMoreInteractions(mockedCallback) + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class, names = ["PENDING"], mode = EnumSource.Mode.EXCLUDE) + fun `M not notify callbacks W updating consent with same value`(fakeConsent: TrackingConsent) { + // GIVEN + testedConsentProvider.registerCallback(mockedCallback) + testedConsentProvider.setConsent(fakeConsent) + + // WHEN + testedConsentProvider.setConsent(fakeConsent) + + // THEN + verify(mockedCallback).onConsentUpdated(TrackingConsent.PENDING, fakeConsent) + verifyNoMoreInteractions(mockedCallback) + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class, names = ["PENDING"], mode = EnumSource.Mode.EXCLUDE) + fun `M unregister callback W requested`(fakeConsent: TrackingConsent) { + // GIVEN + testedConsentProvider.registerCallback(mockedCallback) + + // WHEN + testedConsentProvider.unregisterCallback(mockedCallback) + testedConsentProvider.setConsent(fakeConsent) + + // THEN + verifyNoInteractions(mockedCallback) + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class, names = ["PENDING"], mode = EnumSource.Mode.EXCLUDE) + fun `M unregister all callbacks W requested`(fakeConsent: TrackingConsent) { + // GIVEN + val anotherMockedCallback: TrackingConsentProviderCallback = mock() + testedConsentProvider.registerCallback(anotherMockedCallback) + testedConsentProvider.registerCallback(mockedCallback) + + // WHEN + testedConsentProvider.unregisterAllCallbacks() + testedConsentProvider.setConsent(fakeConsent) + + // THEN + verifyNoInteractions(mockedCallback) + verifyNoInteractions(anotherMockedCallback) + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class, names = ["PENDING"], mode = EnumSource.Mode.EXCLUDE) + fun `M unregister first W called asynchronously`(fakeConsent: TrackingConsent) { + // GIVEN + testedConsentProvider.registerCallback(mockedCallback) + val countDownLatch = CountDownLatch(2) + + // WHEN + Thread { + testedConsentProvider.unregisterAllCallbacks() + countDownLatch.countDown() + }.start() + Thread { + Thread.sleep(1) + testedConsentProvider.setConsent(fakeConsent) + countDownLatch.countDown() + }.start() + + // THEN + verifyNoInteractions(mockedCallback) + } + + @Test + fun `M always return the right value W updating from multiple threads`(forge: Forge) { + // GIVEN + val fakedConsent1 = forge.aValueFrom(TrackingConsent::class.java) + val fakedConsent2 = + forge.aValueFrom(TrackingConsent::class.java, listOf(TrackingConsent.PENDING)) + val countDownLatch = CountDownLatch(2) + + // WHEN + Thread { + testedConsentProvider.setConsent(fakedConsent1) + countDownLatch.countDown() + }.start() + Thread { + Thread.sleep(10) // just to give time to the first thread + testedConsentProvider.setConsent(fakedConsent2) + countDownLatch.countDown() + }.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // THEN + assertThat(testedConsentProvider.getConsent()).isEqualTo(fakedConsent2) + } + + @Test + fun `M notify the registered callback W registering from different threads`() { + // GIVEN + val fakeConsent1 = TrackingConsent.GRANTED + val fakeConsent2 = TrackingConsent.NOT_GRANTED + val countDownLatch = CountDownLatch(3) + + // WHEN + Thread { + testedConsentProvider.registerCallback(mockedCallback) + countDownLatch.countDown() + }.start() + Thread { + Thread.sleep(10) // just to callback register thread to take the lock + testedConsentProvider.setConsent(fakeConsent1) + countDownLatch.countDown() + }.start() + Thread { + Thread.sleep(10) + testedConsentProvider.setConsent(fakeConsent2) + countDownLatch.countDown() + }.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // THEN + assertThat(testedConsentProvider.getConsent()).isIn(fakeConsent1, fakeConsent2) + verify(mockedCallback).onConsentUpdated( + argForWhich { + this == TrackingConsent.PENDING || this == fakeConsent2 + }, + eq(fakeConsent1) + ) + verify(mockedCallback).onConsentUpdated( + argForWhich { + this == TrackingConsent.PENDING || this == fakeConsent1 + }, + eq(fakeConsent2) + ) + verifyNoMoreInteractions(mockedCallback) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/CurlInterceptorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/CurlInterceptorTest.kt new file mode 100644 index 0000000000..2caf73ef97 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/CurlInterceptorTest.kt @@ -0,0 +1,270 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.AdvancedForgery +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class CurlInterceptorTest { + + lateinit var testedInterceptor: Interceptor + + @Mock + lateinit var mockChain: Interceptor.Chain + + @Mock + lateinit var mockOutput: (String) -> Unit + + @StringForgery(regex = "[a-z]+\\.[a-z]{2,3}") + lateinit var fakeHost: String + + @StringForgery(regex = "([a-z0-9_-]+/){0,4}") + lateinit var fakePath: String + + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery( + string = [ + StringForgery(StringForgeryType.ALPHA_NUMERICAL), + StringForgery(StringForgeryType.ALPHABETICAL), + StringForgery(StringForgeryType.HEXADECIMAL), + StringForgery(StringForgeryType.NUMERICAL) + ] + ) + ) + lateinit var fakeQueryParams: Map + + lateinit var fakeUrl: String + + lateinit var fakeRequest: Request + lateinit var fakeResponse: Response + + @StringForgery + lateinit var fakeBody: String + + @BeforeEach + fun `set up`() { + val qp = fakeQueryParams.map { "${it.key}=${it.value}" }.joinToString("&") + fakeUrl = "https://$fakehost/$fakePath?$qp" + } + + @Test + fun `M output curl command W intercept() {GET}`( + @IntForgery(200, 600) statusCode: Int, + @BoolForgery withBody: Boolean + ) { + // Given + testedInterceptor = CurlInterceptor(withBody, mockOutput) + fakeRequest = Request.Builder() + .url(http://github.com/fakeUrl) + .get() + .build() + fakeResponse = forgeResponse(statusCode) + stubChain() + + // When + val response = testedInterceptor.intercept(mockChain) + + // Then + verify(mockChain).proceed(fakeRequest) + assertThat(response).isSameAs(fakeResponse) + verify(mockOutput).invoke("curl -X GET \"$fakeUrl\"") + } + + @Test + fun `M output curl command W intercept() {POST, no body}`( + @IntForgery(200, 600) statusCode: Int + ) { + // Given + testedInterceptor = CurlInterceptor(false, mockOutput) + fakeRequest = Request.Builder() + .url(http://github.com/fakeUrl) + .post(fakeBody.toByteArray().toRequestBody(null)) + .build() + fakeResponse = forgeResponse(statusCode) + stubChain() + + // When + val response = testedInterceptor.intercept(mockChain) + + // Then + verify(mockChain).proceed(fakeRequest) + assertThat(response).isSameAs(fakeResponse) + verify(mockOutput).invoke("curl -X POST \"$fakeUrl\"") + } + + @Test + fun `M output curl command W intercept() {POST, body}`( + @IntForgery(200, 600) statusCode: Int + ) { + // Given + testedInterceptor = CurlInterceptor(true, mockOutput) + fakeRequest = Request.Builder() + .url(http://github.com/fakeUrl) + .post(fakeBody.toByteArray().toRequestBody(null)) + .build() + fakeResponse = forgeResponse(statusCode) + stubChain() + + // When + val response = testedInterceptor.intercept(mockChain) + + // Then + verify(mockChain).proceed(fakeRequest) + assertThat(response).isSameAs(fakeResponse) + verify(mockOutput).invoke("curl -X POST -d '$fakeBody' \"$fakeUrl\"") + } + + @Test + fun `M output curl command W intercept() {GET with headers}`( + @IntForgery(200, 600) statusCode: Int, + @BoolForgery withBody: Boolean, + @StringForgery(StringForgeryType.ALPHABETICAL) headerName: String, + @StringForgery headerValue: String + ) { + // Given + testedInterceptor = CurlInterceptor(withBody, mockOutput) + fakeRequest = Request.Builder() + .url(http://github.com/fakeUrl) + .get() + .addHeader(headerName, headerValue) + .build() + fakeResponse = forgeResponse(statusCode) + stubChain() + + // When + val response = testedInterceptor.intercept(mockChain) + + // Then + verify(mockChain).proceed(fakeRequest) + assertThat(response).isSameAs(fakeResponse) + verify(mockOutput).invoke( + "curl -X GET -H \"${headerName.lowercase(Locale.US)}:$headerValue\" \"$fakeUrl\"" + ) + } + + @Test + fun `M output curl command W intercept() {POST, content type}`( + @IntForgery(200, 600) statusCode: Int, + @StringForgery type: String, + @StringForgery subtype: String + ) { + // Given + testedInterceptor = CurlInterceptor(true, mockOutput) + fakeRequest = Request.Builder() + .url(http://github.com/fakeUrl) + .post(fakeBody.toByteArray().toRequestBody("$type/$subtype".toMediaTypeOrNull())) + .build() + fakeResponse = forgeResponse(statusCode) + stubChain() + + // When + val response = testedInterceptor.intercept(mockChain) + + // Then + verify(mockChain).proceed(fakeRequest) + assertThat(response).isSameAs(fakeResponse) + verify(mockOutput) + .invoke("curl -X POST -H \"Content-Type:$type/$subtype\" -d '$fakeBody' \"$fakeUrl\"") + } + + @Test + fun `M output curl command W intercept() {POST, multipart body}`( + @IntForgery(200, 600) statusCode: Int, + @StringForgery type: String, + @StringForgery subtype: String, + @StringForgery fakeFormKey: String, + @StringForgery fakeFormKeyValue: String + ) { + // Given + testedInterceptor = CurlInterceptor(true, mockOutput) + fakeRequest = Request.Builder() + .url(http://github.com/fakeUrl) + .post( + MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart(fakeFormKey, fakeFormKeyValue) + .addPart( + fakeBody.toByteArray() + .toRequestBody("$type/$subtype".toMediaTypeOrNull()) + ) + .build() + ) + .build() + fakeResponse = forgeResponse(statusCode) + stubChain() + + // When + val response = testedInterceptor.intercept(mockChain) + + // Then + verify(mockChain).proceed(fakeRequest) + assertThat(response).isSameAs(fakeResponse) + val fakeEscapedUrl = fakeUrl + .replace("/", "\\/") + .replace("?", "\\?") + val expectedOutput = "curl -X POST -H \"Content-Type:multipart\\/form-data; " + + "boundary=(.*) -H \"content-disposition:\\[form\\-data; name=\"$fakeFormKey\"]\" " + + "-d '$fakeFormKeyValue' -d '$fakeBody' \"$fakeEscapedUrl\"" + val argumentCaptor = argumentCaptor() + verify(mockOutput).invoke(argumentCaptor.capture()) + assertThat(argumentCaptor.firstValue).matches(expectedOutput) + } + + // region Internal + + private fun forgeResponse(statusCode: Int): Response { + val builder = Response.Builder() + .request(fakeRequest) + .protocol(Protocol.HTTP_2) + .code(statusCode) + .message("{}") + return builder.build() + } + + private fun stubChain() { + whenever(mockChain.request()) doReturn fakeRequest + whenever(mockChain.proceed(any())) doReturn fakeResponse + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataFlusherTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataFlusherTest.kt new file mode 100644 index 0000000000..0de0cd5f3c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataFlusherTest.kt @@ -0,0 +1,210 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.ContextProvider +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FileReader +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReader +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataFlusherTest { + lateinit var testedFlusher: DataFlusher + + @Mock + lateinit var mockDataUploader: DataUploader + + @Mock + lateinit var mockFileOrchestrator: FileOrchestrator + + @Mock + lateinit var mockFileReader: BatchFileReader + + @Mock + lateinit var mockMetaFileReader: FileReader + + @Mock + lateinit var mockFileMover: FileMover + + @Mock + lateinit var mockContextProvider: ContextProvider + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Forgery + lateinit var fakeContext: DatadogContext + + @BeforeEach + fun `set up`() { + whenever(mockContextProvider.getContext(emptySet())) doReturn fakeContext + + testedFlusher = DataFlusher( + mockContextProvider, + mockFileOrchestrator, + mockFileReader, + mockMetaFileReader, + mockFileMover, + mockInternalLogger + ) + } + + @Test + fun `M upload all the batches W flush`( + forge: Forge + ) { + // Given + val fakeFiles = forge.aList { mock() } + val fakeMetaFiles = + forge.aList(fakeFiles.size) { + forge.aNullable { mock().apply { whenever(exists()) doReturn true } } + } + val fakeBatches = forge + .aList(fakeFiles.size) { + forge + .aList { + forge.aString() + } + .map { RawBatchEvent(it.toByteArray()) } + } + val fakeMeta = fakeMetaFiles.map { if (it != null) forge.aString().toByteArray() else null } + whenever(mockFileOrchestrator.getFlushableFiles()).thenReturn(fakeFiles) + fakeFiles.forEachIndexed { index, file -> + whenever( + mockFileReader.readData(file) + ).thenReturn(fakeBatches[index]) + val fakeMetaFile = fakeMetaFiles[index] + whenever( + mockFileOrchestrator.getMetadataFile(file) + ).thenReturn(fakeMetaFile) + if (fakeMetaFile != null) { + whenever(mockMetaFileReader.readData(fakeMetaFile)).thenReturn(fakeMeta[index]) + } + } + + // When + testedFlusher.flush(mockDataUploader) + + // Then + fakeBatches.forEachIndexed { index, batch -> + verify(mockDataUploader).upload(fakeContext, batch, fakeMeta[index]) + } + } + + @Test + fun `M delete all the batches W flush`( + forge: Forge + ) { + // Given + val fakeFiles = forge.aList { mock() } + val fakeMetaFiles = + forge.aList(fakeFiles.size) { mock().apply { whenever(exists()) doReturn true } } + + val fakeBatches = forge + .aList(fakeFiles.size) { + forge + .aList { + forge.aString() + } + .map { RawBatchEvent(it.toByteArray()) } + } + whenever(mockFileOrchestrator.getFlushableFiles()).thenReturn(fakeFiles) + fakeFiles.forEachIndexed { index, file -> + whenever( + mockFileReader.readData(file) + ).thenReturn(fakeBatches[index]) + whenever( + mockFileOrchestrator.getMetadataFile(file) + ).thenReturn(fakeMetaFiles[index]) + } + + // When + testedFlusher.flush(mockDataUploader) + + // Then + fakeFiles.forEach { + verify(mockFileMover).delete(it) + } + fakeMetaFiles.forEach { + verify(mockFileMover).delete(it) + } + } + + @Test + fun `M not attempt to read metadata W flush { meta doesn't exist }`( + forge: Forge + ) { + // Given + val fakeFiles = forge.aList { mock() } + val fakeBatches = forge + .aList(fakeFiles.size) { + forge + .aList { + forge.aString() + } + .map { RawBatchEvent(it.toByteArray()) } + } + whenever(mockFileOrchestrator.getFlushableFiles()).thenReturn(fakeFiles) + fakeFiles.forEachIndexed { index, file -> + whenever( + mockFileReader.readData(file) + ).thenReturn(fakeBatches[index]) + val fakeBatchFile = + forge.aNullable { mock().apply { whenever(exists()) doReturn false } } + whenever( + mockFileOrchestrator.getMetadataFile(file) + ).thenReturn(fakeBatchFile) + } + + // When + testedFlusher.flush(mockDataUploader) + + // Then + verifyNoInteractions(mockMetaFileReader) + } + + @Test + fun `M do nothing W flush { no data available }`() { + // Given + whenever(mockFileOrchestrator.getFlushableFiles()).thenReturn(emptyList()) + + // When + testedFlusher.flush(mockDataUploader) + + // Then + verifyNoInteractions(mockFileReader) + verifyNoInteractions(mockDataUploader) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploaderTest.kt new file mode 100644 index 0000000000..1c8e23da73 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploaderTest.kt @@ -0,0 +1,925 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.net.RequestExecutionContext +import com.datadog.android.api.net.RequestFactory +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.BatchId +import com.datadog.android.core.internal.system.AndroidInfoProvider +import com.datadog.android.internal.profiler.ExecutionTimer +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Call +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.IOException +import java.net.UnknownHostException +import java.util.Locale +import com.datadog.android.api.net.Request as DatadogRequest + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataOkHttpUploaderTest { + + private lateinit var testedUploader: DataOkHttpUploader + + @Mock + lateinit var mockRequestFactory: RequestFactory + + @Mock + lateinit var mockLogger: InternalLogger + + @Mock + lateinit var mockCallFactory: Call.Factory + + @Mock + lateinit var mockCall: Call + + @Mock + lateinit var mockAndroidInfoProvider: AndroidInfoProvider + + @StringForgery + lateinit var fakeSdkVersion: String + + @StringForgery + lateinit var fakeDeviceModel: String + + @StringForgery + lateinit var fakeDeviceBuildId: String + + @StringForgery + lateinit var fakeDeviceVersion: String + + @Forgery + lateinit var fakeContext: DatadogContext + + @StringForgery(regex = "https://[a-z]+\\.com/api") + lateinit var fakeEndpoint: String + + @StringForgery + lateinit var fakeRequestId: String + + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) + lateinit var fakeHeaders: Map + + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]) + ) + lateinit var fakeQueryParams: Map + + @StringForgery + lateinit var fakeRequestContext: String + + @StringForgery + lateinit var fakeRequestBody: String + + private lateinit var fakeResponse: Response + + private lateinit var fakeDatadogRequest: DatadogRequest + + private lateinit var fakeSystemUserAgent: String + + private lateinit var fakeSdkUserAgent: String + + private var fakeBatchId: BatchId? = null + + @Spy + private val mockExecutionTimer: ExecutionTimer = MockExecutionTimer() + + @BeforeEach + fun `set up`(forge: Forge) { + fakeBatchId = forge.aNullable { getForgery() } + whenever(mockCallFactory.newCall(any())) doReturn mockCall + + whenever(mockAndroidInfoProvider.osVersion) doReturn fakeDeviceVersion + whenever(mockAndroidInfoProvider.deviceModel) doReturn fakeDeviceModel + whenever(mockAndroidInfoProvider.deviceBuildId) doReturn fakeDeviceBuildId + + val fakeContentType = forge.anElementFrom( + "multipart/form-data", + "application/json", + "application/x-www-form-urlencoded" + ) + val url = if (forge.aBool()) { + fakeEndpoint + } else { + fakeEndpoint.plus("?" + fakeQueryParams.map { "${it.key}=${it.value}" }) + } + // We need to remove duplicates from the map using case-insensitive comparison, + // because OkHttp will lowercase headers + fakeHeaders = fakeHeaders.entries + .groupBy { it.key.lowercase(Locale.US) } + .mapValues { it.value.first().value } + fakeDatadogRequest = DatadogRequest( + id = fakeRequestId, + description = fakeRequestContext, + headers = fakeHeaders, + url = url, + body = fakeRequestBody.toByteArray(), + contentType = forge.aNullable { fakeContentType } + ) + whenever(mockRequestFactory.create(eq(fakeContext), any(), any(), any())) doReturn + fakeDatadogRequest + + fakeSystemUserAgent = if (forge.aBool()) forge.anAlphaNumericalString() else "" + System.setProperty("http.agent", fakeSystemUserAgent) + + fakeSdkUserAgent = "Datadog/$fakeSdkVersion " + + "(Linux; U; Android $fakeDeviceVersion; " + + "$fakeDeviceModel Build/$fakeDeviceBuildId)" + + testedUploader = DataOkHttpUploader( + requestFactory = mockRequestFactory, + internalLogger = mockLogger, + callFactory = mockCallFactory, + sdkVersion = fakeSdkVersion, + androidInfoProvider = mockAndroidInfoProvider, + executionTimer = mockExecutionTimer + ) + } + + @Test + fun `M redact invalid user agent header W upload()`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery(regex = "[a-z]+") validValue: String, + @StringForgery(regex = "[\u007F-\u00FF]+") invalidValuePostfix: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + + fakeSystemUserAgent = validValue + invalidValuePostfix + System.setProperty("http.agent", fakeSystemUserAgent) + whenever(mockCall.execute()) doReturn mockResponse(202, "{}") + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.Success::class.java) + assertThat(result.code).isEqualTo(202) + verifyRequest(fakeDatadogRequest, expectedUserAgentHeader = validValue) + verifyResponseIsClosed() + } + + @Test + fun `M replace fully invalid user agent header W upload()`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery(regex = "[\u007F-\u00FF]+") invalidValue: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + + fakeSystemUserAgent = invalidValue + System.setProperty("http.agent", fakeSystemUserAgent) + whenever(mockCall.execute()) doReturn mockResponse(202, "{}") + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.Success::class.java) + assertThat(result.code).isEqualTo(202) + verifyRequest(fakeDatadogRequest, expectedUserAgentHeader = fakeSdkUserAgent) + verifyResponseIsClosed() + } + + @Test + fun `M return client token error W upload() { bad api key format }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery(regex = "[\u007F-\u00FF]+") invalidValue: String, + forge: Forge + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + fakeDatadogRequest = fakeDatadogRequest.copy( + headers = fakeDatadogRequest.headers.toMutableMap().apply { + put(RequestFactory.HEADER_API_KEY, forge.anElementFrom("", invalidValue)) + } + ) + whenever(mockRequestFactory.create(eq(fakeContext), any(), eq(batch), eq(batchMetadata))) doReturn + fakeDatadogRequest + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.InvalidTokenError::class.java) + assertThat(result.code).isEqualTo(UploadStatus.UNKNOWN_RESPONSE_CODE) + verifyNoInteractions(mockCallFactory) + } + + // region Expected status codes + + @Test + fun `M return success W upload() {202 accepted status} `( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(202, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.Success::class.java) + assertThat(result.code).isEqualTo(202) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return client error W upload() {400 bad request status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(400, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpClientError::class.java) + assertThat(result.code).isEqualTo(400) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return client token error W upload() {401 unauthorized status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(401, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.InvalidTokenError::class.java) + assertThat(result.code).isEqualTo(401) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return client error W upload() {403 forbidden status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(403, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.InvalidTokenError::class.java) + assertThat(result.code).isEqualTo(403) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return client error with retry W upload() {408 timeout status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(408, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpClientRateLimiting::class.java) + assertThat(result.code).isEqualTo(408) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return client error W upload() {413 too large status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(413, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpClientError::class.java) + assertThat(result.code).isEqualTo(413) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return client error with retry W upload() {429 too many requests status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(429, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpClientRateLimiting::class.java) + assertThat(result.code).isEqualTo(429) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return server error W upload() {500 internal error status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(500, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpServerError::class.java) + assertThat(result.code).isEqualTo(500) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return server error W upload() {502 bad gateway status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(502, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpServerError::class.java) + assertThat(result.code).isEqualTo(502) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return server error W upload() {503 unavailable status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(503, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpServerError::class.java) + assertThat(result.code).isEqualTo(503) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return server error W upload() {504 gateway timeout status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(504, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpServerError::class.java) + assertThat(result.code).isEqualTo(504) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return server error W upload() {507 insufficient storage status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(507, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.HttpServerError::class.java) + assertThat(result.code).isEqualTo(507) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + // endregion + + // region Unknown cases + + @RepeatedTest(32) + fun `M return unknown error W upload() {xxx status}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + forge: Forge, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + + var statusCode: Int + do { + statusCode = forge.anInt(200, 600) + } while (statusCode in arrayOf(202, 400, 401, 403, 408, 413, 429, 500, 502, 503, 504, 507)) + whenever(mockCall.execute()) doReturn mockResponse(statusCode, message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.UnknownHttpError::class.java) + assertThat(result.code).isEqualTo(statusCode) + verifyRequest(fakeDatadogRequest) + verifyResponseIsClosed() + } + + @Test + fun `M return error W upload() {IOException}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doThrow IOException(message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.NetworkError::class.java) + assertThat(result.code).isEqualTo(UploadStatus.UNKNOWN_RESPONSE_CODE) + verifyRequest(fakeDatadogRequest) + } + + @Test + fun `M return error W upload() {UnknownHostException}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doThrow UnknownHostException(message) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.DNSError::class.java) + assertThat(result.code).isEqualTo(UploadStatus.UNKNOWN_RESPONSE_CODE) + verifyRequest(fakeDatadogRequest) + } + + @Test + fun `M return error W upload() {any Throwable}`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @Forgery throwable: Throwable + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doThrow throwable + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.UnknownException::class.java) + assertThat(result.code).isEqualTo(UploadStatus.UNKNOWN_RESPONSE_CODE) + verifyRequest(fakeDatadogRequest) + } + + @Test + fun `M return RequestCreationError W upload() { request factory throws }`( + @Forgery fakeException: Exception, + @Forgery batch: List, + @StringForgery batchMeta: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockRequestFactory.create(eq(fakeContext), any(), eq(batch), eq(batchMetadata))) + .doThrow(fakeException) + + // When + val result = testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + assertThat(result).isInstanceOf(UploadStatus.RequestCreationError::class.java) + assertThat(result.code).isEqualTo(UploadStatus.UNKNOWN_RESPONSE_CODE) + verifyNoInteractions(mockCallFactory) + } + + @Test + fun `M log the exception to telemetry W upload() { request factory throws }`( + @Forgery fakeException: Exception, + @Forgery batch: List, + @StringForgery batchMeta: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockRequestFactory.create(eq(fakeContext), any(), eq(batch), eq(batchMetadata))) + .doThrow(fakeException) + + // When + testedUploader.upload(fakeContext, batch, batchMetadata) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Unable to create the request, probably due to bad data format. " + + "The batch will be dropped.", + fakeException + ) + } + + // endregion + + // region ExecutionContext + + @Test + fun `M pass the ExecutionContext to requestFactory W upload { same batchId retried }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String, + @IntForgery(2, 10) retries: Int, + @Forgery batchId: BatchId, + forge: Forge + ) { + // Given + val statusCodes = forge.aList(size = retries) { forge.anInt(400, 600) } + whenever(mockCall.execute()) doReturnConsecutively statusCodes.map { mockResponse(it, message) } + + // When + repeat(retries) { + testedUploader.upload(fakeContext, batch, batchMeta.toByteArray(), batchId) + } + + // Then + argumentCaptor { + verify(mockRequestFactory, times(retries)) + .create(eq(fakeContext), capture(), eq(batch), eq(batchMeta.toByteArray())) + allValues.forEachIndexed { index, value -> + assertThat(value.attemptNumber).isEqualTo(index + 1) + if (index == 0) { + assertThat(value.previousResponseCode).isNull() + } else { + assertThat(value.previousResponseCode).isEqualTo(statusCodes[index - 1]) + } + } + } + } + + @Test + fun `M pass the ExecutionContext to requestFactory W upload { same batchId retried, new thread each time }`( + // we are testing the same as the previous test, but we are making sure that the state of the uploader + // it is shared between threads (it should be kept in the heap and not thread local memory) + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String, + @IntForgery(2, 10) retries: Int, + @Forgery batchId: BatchId, + forge: Forge + ) { + // Given + val statusCodes = forge.aList(size = retries) { forge.anInt(400, 600) } + whenever(mockCall.execute()) doReturnConsecutively statusCodes.map { mockResponse(it, message) } + + // When + repeat(retries) { + val thread = Thread { + testedUploader.upload(fakeContext, batch, batchMeta.toByteArray(), batchId) + } + thread.start() + thread.join() + } + + // Then + argumentCaptor { + verify(mockRequestFactory, times(retries)) + .create(eq(fakeContext), capture(), eq(batch), eq(batchMeta.toByteArray())) + allValues.forEachIndexed { index, value -> + assertThat(value.attemptNumber).isEqualTo(index + 1) + if (index == 0) { + assertThat(value.previousResponseCode).isNull() + } else { + assertThat(value.previousResponseCode).isEqualTo(statusCodes[index - 1]) + } + } + } + } + + @Test + fun `M pass empty ExecutionContext to requestFactory W upload { null batchId }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String, + @IntForgery(2, 10) retries: Int, + forge: Forge + ) { + // Given + val statusCodes = forge.aList(size = retries) { forge.anInt(400, 600) } + whenever(mockCall.execute()) doReturnConsecutively statusCodes.map { mockResponse(it, message) } + + // When + repeat(retries) { + testedUploader.upload(fakeContext, batch, batchMeta.toByteArray(), null) + } + + // Then + argumentCaptor { + verify(mockRequestFactory, times(retries)) + .create(eq(fakeContext), capture(), eq(batch), eq(batchMeta.toByteArray())) + allValues.forEach { value -> + assertThat(value.attemptNumber).isEqualTo(1) + assertThat(value.previousResponseCode).isNull() + } + } + } + + @Test + fun `M pass the ExecutionContext to requestFactory W upload { different batchId upload }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String, + forge: Forge + ) { + // Given + val batchIds: List = forge.aList(size = forge.anInt(min = 2, max = 10)) { getForgery() } + val statusCodes = forge.aList(size = batchIds.size) { forge.anInt(200, 300) } + whenever(mockCall.execute()) doReturnConsecutively statusCodes.map { mockResponse(it, message) } + + // When + batchIds.forEach { batchId -> + testedUploader.upload(fakeContext, batch, batchMeta.toByteArray(), batchId) + } + + // Then + argumentCaptor { + verify(mockRequestFactory, times(batchIds.size)) + .create(eq(fakeContext), capture(), eq(batch), eq(batchMeta.toByteArray())) + allValues.forEach { value -> + assertThat(value.attemptNumber).isEqualTo(1) + assertThat(value.previousResponseCode).isNull() + } + } + } + + // endregion + + @Test + fun `M log warning W upload() { feature request has user-agent header }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String, + @StringForgery userAgentValue: String, + forge: Forge + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + whenever(mockCall.execute()) doReturn mockResponse(202, message) + + fakeDatadogRequest = fakeDatadogRequest.copy( + headers = fakeDatadogRequest.headers.toMutableMap().apply { + put(forge.anElementFrom("User-Agent", "user-agent", "UsEr-AgEnT"), userAgentValue) + } + ) + + whenever(mockRequestFactory.create(eq(fakeContext), any(), eq(batch), eq(batchMetadata))) doReturn + fakeDatadogRequest + + // When + testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + DataOkHttpUploader.WARNING_USER_AGENT_HEADER_RESERVED + ) + } + + // region Internal + + private fun mockResponse(statusCode: Int, message: String): Response { + fakeResponse = Response.Builder() + .request(Request.Builder().url(http://github.com/fakeEndpoint).get().build()) + .code(statusCode) + .message(message) + .protocol(Protocol.HTTP_2) + .body(mock()) + .build() + return fakeResponse + } + + private fun verifyRequest( + expectedRequest: DatadogRequest, + expectedUserAgentHeader: String = fakeSystemUserAgent.ifBlank { fakeSdkUserAgent } + ) { + argumentCaptor { + verify(mockCallFactory).newCall(capture()) + + verifyRequestUrl(firstValue.url, expectedRequest.url.toHttpUrl()) + verifyRequestHeaders( + firstValue.headers, + expectedRequest.headers, + expectedUserAgentHeader + ) + verifyRequestBody(firstValue.body, expectedRequest.body, expectedRequest.contentType) + } + } + + private fun verifyRequestUrl(url: HttpUrl, expectedUrl: HttpUrl) { + assertThat(url.scheme).isEqualTo(expectedUrl.scheme) + assertThat(url.host).isEqualTo(expectedUrl.host) + assertThat(url.encodedPath).isEqualTo(expectedUrl.encodedPath) + + val expectedQueryParams = expectedUrl.queryParameterNames + + assertThat(url.queryParameterNames.size).isEqualTo(expectedQueryParams.size) + + if (expectedQueryParams.isEmpty()) { + assertThat(url.query).isNullOrEmpty() + } else { + expectedQueryParams.forEach { + val actualValue = url.queryParameter(it) + val expectedValue = expectedUrl.queryParameter(it) + assertThat(actualValue) + .overridingErrorMessage( + "Expected query parameter $it to be equal to [$expectedValue] " + + "but was [$actualValue]" + ) + .isEqualTo(expectedValue) + } + } + } + + private fun verifyRequestBody( + body: RequestBody?, + expectedBody: ByteArray, + contentType: String? + ) { + checkNotNull(body) + if (contentType == null) { + assertThat(body.contentType()).isNull() + } else { + assertThat(body.contentType().toString()).isEqualTo(contentType) + } + assertThat(body.contentLength()).isEqualTo(expectedBody.size.toLong()) + } + + private fun verifyRequestHeaders( + headers: Headers, + expectedHeaders: Map, + expectedUserAgentHeader: String + ) { + val actualHeaders = headers.toMultimap() + + assertThat(actualHeaders.values).allMatch { it.size == 1 } + + assertThat( + actualHeaders + .mapValues { it.value.first() } + .filter { !"User-Agent".equals(it.key, ignoreCase = true) } + ) + .isEqualTo(expectedHeaders.mapKeys { it.key.lowercase(Locale.US) }) + + assertThat(headers["User-Agent"]).isEqualTo(expectedUserAgentHeader) + } + + private fun verifyResponseIsClosed() { + verify(fakeResponse.body)!!.close() + } + + // endregion + + // region benchmark + + @Test + fun `M measure response latency W upload`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @StringForgery message: String + ) { + // Given + val batchMetadata = batchMeta.toByteArray() + val mockSuccess = mockResponse(202, message) + whenever(mockCall.execute()) doReturn mockSuccess + + // When + testedUploader.upload(fakeContext, batch, batchMetadata, fakeBatchId) + + // Then + verify(mockExecutionTimer).measure(any<() -> UploadStatus>()) + } + + // endregion + + class MockExecutionTimer : ExecutionTimer { + override fun measure(action: () -> T): T { + return action() + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnableTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnableTest.kt new file mode 100644 index 0000000000..f76a1491cb --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnableTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.ContextProvider +import com.datadog.android.core.internal.metrics.BenchmarkUploads +import com.datadog.android.core.internal.net.info.NetworkInfoProvider +import com.datadog.android.core.internal.persistence.BatchData +import com.datadog.android.core.internal.persistence.BatchId +import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.system.SystemInfo +import com.datadog.android.core.internal.system.SystemInfoProvider +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mock +import org.mockito.invocation.InvocationOnMock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import org.mockito.stubbing.Answer +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataUploadRunnableTest { + + @Mock + lateinit var mockThreadPoolExecutor: ScheduledThreadPoolExecutor + + @Mock + lateinit var mockStorage: Storage + + @Mock + lateinit var mockDataUploader: DataUploader + + @Mock + lateinit var mockNetworkInfoProvider: NetworkInfoProvider + + @Mock + lateinit var mockSystemInfoProvider: SystemInfoProvider + + @Mock + lateinit var mockBenchmarkUploads: BenchmarkUploads + + @Mock + lateinit var mockContextProvider: ContextProvider + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockUploadSchedulerStrategy: UploadSchedulerStrategy + + @Forgery + lateinit var fakeContext: DatadogContext + + @IntForgery(min = 1, max = 4) + var fakeMaxBatchesPerJob: Int = 0 + + @LongForgery + var fakeDelayUntilNextUploadMs: Long = 0L + + @StringForgery + lateinit var fakeFeatureName: String + + private lateinit var testedRunnable: DataUploadRunnable + + @BeforeEach + fun `set up`(forge: Forge) { + val fakeNetworkInfo = NetworkInfo( + forge.aValueFrom( + enumClass = NetworkInfo.Connectivity::class.java, + exclude = listOf(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + ) + ) + whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo + val fakeSystemInfo = SystemInfo( + batteryFullOrCharging = true, + batteryLevel = forge.anInt(min = 20, max = 100), + powerSaveMode = false, + onExternalPowerSource = true + ) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + whenever(mockUploadSchedulerStrategy.getMsDelayUntilNextUpload(any(), any(), anyOrNull(), anyOrNull())) + .doReturn(fakeDelayUntilNextUploadMs) + + whenever(mockContextProvider.getContext(emptySet())) doReturn fakeContext + + testedRunnable = DataUploadRunnable( + featureName = fakeFeatureName, + threadPoolExecutor = mockThreadPoolExecutor, + storage = mockStorage, + dataUploader = mockDataUploader, + contextProvider = mockContextProvider, + networkInfoProvider = mockNetworkInfoProvider, + systemInfoProvider = mockSystemInfoProvider, + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + internalLogger = mockInternalLogger + ) + } + + @Test + fun `doesn't send batch when offline`() { + // Given + val networkInfo = NetworkInfo( + NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED + ) + whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn networkInfo + + // When + testedRunnable.run() + + // Then + verifyNoInteractions(mockDataUploader, mockStorage) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M send batch W run() { batteryFullOrCharging }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @IntForgery(min = 0, max = DataUploadRunnable.LOW_BATTERY_THRESHOLD) batteryLevel: Int, + forge: Forge + ) { + // Given + val fakeSystemInfo = SystemInfo( + batteryFullOrCharging = true, + batteryLevel = batteryLevel, + onExternalPowerSource = false, + powerSaveMode = false + ) + + val batchId = mock() + val batchMetadata = forge.aNullable { batchMeta.toByteArray() } + + whenever(mockStorage.readNextBatch()).thenReturn(BatchData(batchId, batch, batchMetadata)) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + + val fakeUploadStatus = forge.getForgery(UploadStatus.Success::class.java) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata, + batchId + ) + ) doReturn fakeUploadStatus + + // When + testedRunnable.run() + + // Then + verify(mockStorage, times(fakeMaxBatchesPerJob)).confirmBatchRead( + eq(batchId), + any(), + eq(true) + ) + verify(mockDataUploader, times(fakeMaxBatchesPerJob)).upload(fakeContext, batch, batchMetadata, batchId) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload( + fakeFeatureName, + fakeMaxBatchesPerJob, + fakeUploadStatus.code, + null + ) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M send batch W run() { battery level high }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @IntForgery(min = DataUploadRunnable.LOW_BATTERY_THRESHOLD + 1) batteryLevel: Int, + forge: Forge + ) { + // Given + val fakeSystemInfo = SystemInfo( + batteryLevel = batteryLevel, + batteryFullOrCharging = false, + onExternalPowerSource = false, + powerSaveMode = false + ) + val batchId = mock() + val batchMetadata = forge.aNullable { batchMeta.toByteArray() } + + whenever(mockStorage.readNextBatch()).thenReturn(BatchData(batchId, batch, batchMetadata)) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + + val fakeUploadStatus = forge.getForgery(UploadStatus.Success::class.java) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata, + batchId + ) + ) doReturn fakeUploadStatus + + // When + testedRunnable.run() + + // Then + verify(mockStorage, times(fakeMaxBatchesPerJob)).confirmBatchRead( + eq(batchId), + any(), + eq(true) + ) + verify(mockDataUploader, times(fakeMaxBatchesPerJob)).upload(fakeContext, batch, batchMetadata, batchId) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload( + fakeFeatureName, + fakeMaxBatchesPerJob, + fakeUploadStatus.code, + null + ) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M send batch W run() { onExternalPower }`( + @Forgery batch: List, + @StringForgery batchMeta: String, + @IntForgery(min = 0, max = DataUploadRunnable.LOW_BATTERY_THRESHOLD) batteryLevel: Int, + forge: Forge + ) { + val fakeSystemInfo = SystemInfo( + onExternalPowerSource = true, + batteryLevel = batteryLevel, + batteryFullOrCharging = false, + powerSaveMode = false + ) + val batchId = mock() + val batchMetadata = forge.aNullable { batchMeta.toByteArray() } + + whenever(mockStorage.readNextBatch()).thenReturn(BatchData(batchId, batch, batchMetadata)) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + + val fakeUploadStatus = forge.getForgery(UploadStatus.Success::class.java) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata, + batchId + ) + ) doReturn fakeUploadStatus + + // When + testedRunnable.run() + + // Then + verify(mockStorage, times(fakeMaxBatchesPerJob)).confirmBatchRead( + eq(batchId), + any(), + eq(true) + ) + verify(mockDataUploader, times(fakeMaxBatchesPerJob)).upload(fakeContext, batch, batchMetadata, batchId) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload( + fakeFeatureName, + fakeMaxBatchesPerJob, + fakeUploadStatus.code, + null + ) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M not send batch W run() { not enough battery }`( + @IntForgery(min = 0, max = DataUploadRunnable.LOW_BATTERY_THRESHOLD) batteryLevel: Int + ) { + // Given + val fakeSystemInfo = SystemInfo( + batteryLevel = batteryLevel, + batteryFullOrCharging = false, + onExternalPowerSource = false, + powerSaveMode = false + ) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + + // When + testedRunnable.run() + + // Then + verifyNoInteractions(mockStorage, mockDataUploader) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M not send batch W run() { batteryFullOrCharging, powerSaveMode }`( + @IntForgery(min = 0, max = 100) batteryLevel: Int + ) { + // Given + val fakeSystemInfo = SystemInfo( + batteryFullOrCharging = true, + powerSaveMode = true, + batteryLevel = batteryLevel, + onExternalPowerSource = false + ) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + + // When + testedRunnable.run() + + // Then + verifyNoInteractions(mockStorage, mockDataUploader) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M not send batch W run() { batteryLeveHigh, powerSaveMode }`( + @IntForgery(min = DataUploadRunnable.LOW_BATTERY_THRESHOLD + 1) batteryLevel: Int + ) { + // Given + val fakeSystemInfo = SystemInfo( + batteryLevel = batteryLevel, + powerSaveMode = true, + batteryFullOrCharging = false, + onExternalPowerSource = false + ) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + + // When + testedRunnable.run() + + // Then + verifyNoInteractions(mockStorage, mockDataUploader) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M not send batch W run() { onExternalPower, powerSaveMode }`( + @IntForgery(min = 0, max = DataUploadRunnable.LOW_BATTERY_THRESHOLD) batteryLevel: Int + ) { + // Given + val fakeSystemInfo = SystemInfo( + onExternalPowerSource = true, + powerSaveMode = true, + batteryLevel = batteryLevel, + batteryFullOrCharging = false + ) + whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo + + // When + testedRunnable.run() + + // Then + verifyNoInteractions(mockStorage, mockDataUploader) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M do nothing W no batch to send`() { + // Given + whenever(mockStorage.readNextBatch()).thenReturn(null) + + // When + testedRunnable.run() + + // Then + verify(mockStorage).readNextBatch() + verifyNoMoreInteractions(mockStorage) + verifyNoInteractions(mockDataUploader) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `batch sent successfully`( + @Forgery batch: List, + @StringForgery batchMeta: String, + forge: Forge + ) { + // Given + val batchId = mock() + val batchMetadata = forge.aNullable { batchMeta.toByteArray() } + + whenever(mockStorage.readNextBatch()).thenReturn(BatchData(batchId, batch, batchMetadata)) + val fakeUploadStatus = forge.getForgery(UploadStatus.Success::class.java) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata, + batchId + ) + ) doReturn fakeUploadStatus + + // When + testedRunnable.run() + + // Then + verify(mockStorage, times(fakeMaxBatchesPerJob)).confirmBatchRead(any(), any(), eq(true)) + verify(mockDataUploader, times(fakeMaxBatchesPerJob)).upload(fakeContext, batch, batchMetadata, batchId) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload( + fakeFeatureName, + fakeMaxBatchesPerJob, + fakeUploadStatus.code, + null + ) + verify(mockThreadPoolExecutor).remove(testedRunnable) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @ParameterizedTest + @MethodSource("retryBatchStatusValues") + fun `batch kept on error`( + uploadStatus: UploadStatus, + @Forgery batch: List, + @StringForgery batchMeta: String, + forge: Forge + ) { + // Given + val batchId = mock() + val batchMetadata = forge.aNullable { batchMeta.toByteArray() } + + whenever(mockStorage.readNextBatch()).thenReturn(BatchData(batchId, batch, batchMetadata)) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata, + batchId + ) + ) doReturn uploadStatus + + // When + testedRunnable.run() + + // Then + verify(mockStorage).confirmBatchRead(eq(batchId), any(), eq(false)) + verify(mockDataUploader).upload(fakeContext, batch, batchMetadata, batchId) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload( + fakeFeatureName, + 1, + uploadStatus.code, + uploadStatus.throwable + ) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @ParameterizedTest + @MethodSource("retryBatchStatusValues") + fun `batch kept after n errors`( + uploadStatus: UploadStatus, + @Forgery batch: List, + @StringForgery batchMeta: String, + @IntForgery(min = 3, max = 42) runCount: Int, + forge: Forge + ) { + // Given + val batchId = mock() + val batchMetadata = forge.aNullable { batchMeta.toByteArray() } + + whenever(mockStorage.readNextBatch()).thenReturn(BatchData(batchId, batch, batchMetadata)) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata, + batchId + ) + ) doReturn uploadStatus + + // WHen + repeat(runCount) { + testedRunnable.run() + } + + // Then + verify(mockStorage, times(runCount)).confirmBatchRead(eq(batchId), any(), eq(false)) + verify(mockDataUploader, times(runCount)).upload(fakeContext, batch, batchMetadata, batchId) + verify(mockUploadSchedulerStrategy, times(runCount)).getMsDelayUntilNextUpload( + fakeFeatureName, + 1, + uploadStatus.code, + uploadStatus.throwable + ) + verify(mockThreadPoolExecutor, times(runCount)) + .schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @ParameterizedTest + @MethodSource("dropBatchStatusValues") + fun `batch dropped on error`( + uploadStatus: UploadStatus, + @Forgery batch: List, + @StringForgery batchMeta: String, + forge: Forge + ) { + // Given + val batchId = mock() + val batchMetadata = forge.aNullable { batchMeta.toByteArray() } + + whenever(mockStorage.readNextBatch()).thenReturn(BatchData(batchId, batch, batchMetadata)) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata, + batchId + ) + ) doReturn uploadStatus + + // When + testedRunnable.run() + + // Then + verify(mockStorage).confirmBatchRead(eq(batchId), any(), eq(true)) + verify(mockDataUploader).upload(fakeContext, batch, batchMetadata, batchId) + verify(mockUploadSchedulerStrategy).getMsDelayUntilNextUpload( + fakeFeatureName, + 1, + uploadStatus.code, + uploadStatus.throwable + ) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + // region maxBatchesPerJob + + @Test + fun `M handle the maxBatchesPerJob W run{maxBatchesPerJob smaller availableBatches}`( + forge: Forge + ) { + // Given + testedRunnable = DataUploadRunnable( + featureName = fakeFeatureName, + threadPoolExecutor = mockThreadPoolExecutor, + storage = mockStorage, + dataUploader = mockDataUploader, + contextProvider = mockContextProvider, + networkInfoProvider = mockNetworkInfoProvider, + systemInfoProvider = mockSystemInfoProvider, + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + internalLogger = mockInternalLogger + ) + val batches = forge.aList( + size = forge.anInt( + min = fakeMaxBatchesPerJob + 1, + max = fakeMaxBatchesPerJob + 1000 + ) + ) { + aList { getForgery() } + } + val batchIds: List = batches.map { mock() } + val batchMetadata = forge.aList(size = batches.size) { aNullable { aString().toByteArray() } } + stubStorage(batchIds, batches, batchMetadata) + batches.forEachIndexed { index, batch -> + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata[index], + batchIds[index] + ) + ) doReturn forge.getForgery(UploadStatus.Success::class.java) + } + + // When + testedRunnable.run() + + // Then + repeat(fakeMaxBatchesPerJob) { index -> + val batch = batches[index] + verify(mockDataUploader).upload(fakeContext, batch, batchMetadata[index], batchIds[index]) + verify(mockStorage).confirmBatchRead( + eq(batchIds[index]), + any(), + eq(true) + ) + } + verifyNoMoreInteractions(mockDataUploader) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + @Test + fun `M exhaust the available batches W run {maxBatchesPerJob higher or equal availableBatches}`( + forge: Forge + ) { + // Given + testedRunnable = DataUploadRunnable( + featureName = fakeFeatureName, + threadPoolExecutor = mockThreadPoolExecutor, + storage = mockStorage, + dataUploader = mockDataUploader, + contextProvider = mockContextProvider, + networkInfoProvider = mockNetworkInfoProvider, + systemInfoProvider = mockSystemInfoProvider, + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + internalLogger = mockInternalLogger + ) + val fakeBatchesCount = forge.anInt( + min = 1, + max = fakeMaxBatchesPerJob + 1 + ) + val batches = forge.aList(size = fakeBatchesCount) { aList { getForgery() } } + val batchIds: List = batches.map { mock() } + val batchMetadata = forge.aList(size = batches.size) { aNullable { aString().toByteArray() } } + stubStorage(batchIds, batches, batchMetadata) + val fakeUploadStatus = forge.getForgery(UploadStatus.Success::class.java) + + batches.forEachIndexed { index, batch -> + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMetadata[index], + batchIds[index] + ) + ) doReturn fakeUploadStatus + } + + // When + testedRunnable.run() + + // Then + batches.forEachIndexed { index, batch -> + verify(mockDataUploader).upload(fakeContext, batch, batchMetadata[index], batchIds[index]) + verify(mockStorage).confirmBatchRead( + eq(batchIds[index]), + any(), + eq(true) + ) + } + verifyNoMoreInteractions(mockDataUploader) + verify(mockThreadPoolExecutor).schedule(testedRunnable, fakeDelayUntilNextUploadMs, TimeUnit.MILLISECONDS) + } + + // region Internal + + private fun stubStorage( + batchIds: List, + batches: List>, + batchMeta: List + ) { + reset(mockStorage) + whenever(mockStorage.readNextBatch()) doAnswer object : Answer { + var index = 0 + + override fun answer(invocation: InvocationOnMock): BatchData? { + val data = if (index >= batches.size) { + null + } else { + val batchData = BatchData(batchIds[index], batches[index], batchMeta[index]) + batchData + } + index++ + return data + } + } + } + + // endregion + + // region sdkBenchmarks + + @Test + fun `M send upload benchmark telemetry W run { online }`() { + // Given + testedRunnable = DataUploadRunnable( + featureName = fakeFeatureName, + threadPoolExecutor = mockThreadPoolExecutor, + storage = mockStorage, + dataUploader = mockDataUploader, + contextProvider = mockContextProvider, + networkInfoProvider = mockNetworkInfoProvider, + systemInfoProvider = mockSystemInfoProvider, + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + internalLogger = mockInternalLogger, + benchmarkUploads = mockBenchmarkUploads + ) + + // When + testedRunnable.run() + + // Then + verify(mockBenchmarkUploads).incrementBenchmarkUploadsCount(any()) + } + + @Test + fun `M not send upload benchmark telemetry W run { offline }`( + @Mock mockNetworkInfo: NetworkInfo + ) { + // Given + testedRunnable = DataUploadRunnable( + featureName = fakeFeatureName, + threadPoolExecutor = mockThreadPoolExecutor, + storage = mockStorage, + dataUploader = mockDataUploader, + contextProvider = mockContextProvider, + networkInfoProvider = mockNetworkInfoProvider, + systemInfoProvider = mockSystemInfoProvider, + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + internalLogger = mockInternalLogger, + benchmarkUploads = mockBenchmarkUploads + ) + + whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) + .thenReturn(mockNetworkInfo) + + whenever(mockNetworkInfo.connectivity) + .thenReturn(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + + // When + testedRunnable.run() + + // Then + verify(mockBenchmarkUploads, never()).incrementBenchmarkUploadsCount(any()) + } + + @Test + fun `M send bytes uploaded benchmark telemetry W run { successful upload }`( + forge: Forge + ) { + // Given + testedRunnable = DataUploadRunnable( + featureName = fakeFeatureName, + threadPoolExecutor = mockThreadPoolExecutor, + storage = mockStorage, + dataUploader = mockDataUploader, + contextProvider = mockContextProvider, + networkInfoProvider = mockNetworkInfoProvider, + systemInfoProvider = mockSystemInfoProvider, + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + internalLogger = mockInternalLogger, + benchmarkUploads = mockBenchmarkUploads + ) + + val batch = forge.aList { getForgery() } + val batchMeta = forge.anAsciiString() + val batchId = mock() + val batchData = BatchData(batchId, batch, batchMeta.toByteArray()) + whenever(mockStorage.readNextBatch()) + .thenReturn(batchData) + .thenReturn(null) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMeta.toByteArray(), + batchId + ) + ).thenReturn(UploadStatus.Success(202)) + + // When + testedRunnable.run() + + // Then + verify(mockBenchmarkUploads).sendBenchmarkBytesUploaded(any(), any()) + } + + @Test + fun `M not send bytes uploaded benchmark telemetry W run { failed upload }`( + forge: Forge + ) { + // Given + testedRunnable = DataUploadRunnable( + featureName = fakeFeatureName, + threadPoolExecutor = mockThreadPoolExecutor, + storage = mockStorage, + dataUploader = mockDataUploader, + contextProvider = mockContextProvider, + networkInfoProvider = mockNetworkInfoProvider, + systemInfoProvider = mockSystemInfoProvider, + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + internalLogger = mockInternalLogger, + benchmarkUploads = mockBenchmarkUploads + ) + + val batch = forge.aList { getForgery() } + val batchMeta = forge.anAsciiString() + val batchId = mock() + val batchData = BatchData(batchId, batch, batchMeta.toByteArray()) + whenever(mockStorage.readNextBatch()) + .thenReturn(batchData) + .thenReturn(null) + whenever( + mockDataUploader.upload( + fakeContext, + batch, + batchMeta.toByteArray(), + batchId + ) + ).thenReturn(UploadStatus.RequestCreationError(mock())) + + // When + testedRunnable.run() + + // Then + verify(mockBenchmarkUploads, never()).sendBenchmarkBytesUploaded(any(), any()) + } + + // endregion + + companion object { + + @JvmStatic + fun retryBatchStatusValues(): List { + val forge = Forge().apply { + Configurator().configure(this) + } + + return listOf( + forge.getForgery(UploadStatus.HttpClientRateLimiting::class.java), + forge.getForgery(UploadStatus.HttpServerError::class.java), + forge.getForgery(UploadStatus.NetworkError::class.java), + forge.getForgery(UploadStatus.UnknownException::class.java) + ) + } + + @JvmStatic + fun dropBatchStatusValues(): List { + val forge = Forge().apply { + Configurator().configure(this) + } + + return listOf( + forge.getForgery(UploadStatus.HttpClientError::class.java), + forge.getForgery(UploadStatus.HttpRedirection::class.java), + forge.getForgery(UploadStatus.UnknownHttpError::class.java), + forge.getForgery(UploadStatus.UnknownStatus::class.java) + ) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadSchedulerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadSchedulerTest.kt new file mode 100644 index 0000000000..e66fa553cb --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadSchedulerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.quality.Strictness +import java.util.concurrent.ScheduledThreadPoolExecutor + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataUploadSchedulerTest { + + private lateinit var testedScheduler: DataUploadScheduler + + @Mock + lateinit var mockExecutor: ScheduledThreadPoolExecutor + + @Forgery + lateinit var fakeUploadConfiguration: DataUploadConfiguration + + @StringForgery + lateinit var fakeFeatureName: String + + @Mock + lateinit var mockUploadSchedulerStrategy: UploadSchedulerStrategy + + @IntForgery(min = 1, max = 4) + var fakeMaxBatchesPerJob: Int = 0 + + @BeforeEach + fun `set up`() { + testedScheduler = DataUploadScheduler( + featureName = fakeFeatureName, + storage = mock(), + dataUploader = mock(), + contextProvider = mock(), + networkInfoProvider = mock(), + systemInfoProvider = mock(), + uploadSchedulerStrategy = mockUploadSchedulerStrategy, + maxBatchesPerJob = fakeMaxBatchesPerJob, + scheduledThreadPoolExecutor = mockExecutor, + internalLogger = mock() + ) + } + + @Test + fun `when start it will execute a runnable`() { + // When + testedScheduler.startScheduling() + + // Then + verify(mockExecutor).execute( + argThat { this is DataUploadRunnable } + ) + } + + @Test + fun `when stop it will try to remove the executed runnable`() { + // Given + testedScheduler.startScheduling() + + // When + testedScheduler.stopScheduling() + + // Then + val argumentCaptor = argumentCaptor() + verify(mockExecutor).execute( + argumentCaptor.capture() + ) + verify(mockExecutor).remove(argumentCaptor.firstValue) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategyTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategyTest.kt new file mode 100644 index 0000000000..6aef44bab8 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategyTest.kt @@ -0,0 +1,127 @@ +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.core.configuration.UploadSchedulerStrategy +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.io.IOException + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DefaultUploadSchedulerStrategyTest { + + private lateinit var testedStrategy: UploadSchedulerStrategy + + @Forgery + lateinit var fakeConfiguration: DataUploadConfiguration + + @StringForgery + lateinit var fakeFeatureName: String + + private var initialDelay = 0L + + @BeforeEach + fun `set up`() { + testedStrategy = DefaultUploadSchedulerStrategy(fakeConfiguration) + initialDelay = testedStrategy.getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) + } + + @Test + fun `M decrease delay to minimum W getMsDelayUntilNextUpload() {successful attempt}`( + @IntForgery(1, 128) repeats: Int, + @IntForgery(1, 64) attempts: Int + ) { + // Given + var delay = 0L + + // When + repeat(repeats) { delay = testedStrategy.getMsDelayUntilNextUpload(fakeFeatureName, attempts, 202, null) } + + // Then + assertThat(delay).isEqualTo(fakeConfiguration.minDelayMs) + } + + @Test + fun `M increase delay W getMsDelayUntilNextUpload() {no attempt made}`( + @IntForgery(1, 128) repeats: Int + ) { + // Given + var delay = 0L + + // When + repeat(repeats) { delay = testedStrategy.getMsDelayUntilNextUpload(fakeFeatureName, 0, null, null) } + + // Then + assertThat(delay).isGreaterThan(initialDelay) + assertThat(delay).isLessThanOrEqualTo(fakeConfiguration.maxDelayMs) + } + + @Test + fun `M increase delay W getMsDelayUntilNextUpload() {invalid status code}`( + @IntForgery(1, 128) repeats: Int, + @IntForgery(1, 64) attempts: Int, + @IntForgery(300, 600) statusCode: Int + ) { + // Given + var delay = 0L + + // When + repeat(repeats) { + delay = testedStrategy.getMsDelayUntilNextUpload(fakeFeatureName, attempts, statusCode, null) + } + + // Then + assertThat(delay).isGreaterThan(initialDelay) + assertThat(delay).isLessThanOrEqualTo(fakeConfiguration.maxDelayMs) + } + + @Test + fun `M increase delay W getMsDelayUntilNextUpload() {non IOException}`( + @IntForgery(1, 128) repeats: Int, + @IntForgery(1, 64) attempts: Int, + @Forgery exception: Exception + ) { + // Given + var delay = 0L + + // When + repeat(repeats) { delay = testedStrategy.getMsDelayUntilNextUpload(fakeFeatureName, attempts, null, exception) } + + // Then + assertThat(delay).isGreaterThan(initialDelay) + assertThat(delay).isLessThanOrEqualTo(fakeConfiguration.maxDelayMs) + } + + @Test + fun `M increase delay to high value W getMsDelayUntilNextUpload() {IOException}`( + @IntForgery(1, 128) repeats: Int, + @IntForgery(1, 64) attempts: Int, + @StringForgery message: String + ) { + // Given + var delay = 0L + val exception = IOException(message) + + // When + repeat(repeats) { delay = testedStrategy.getMsDelayUntilNextUpload(fakeFeatureName, attempts, null, exception) } + + // Then + assertThat(delay).isEqualTo(DefaultUploadSchedulerStrategy.NETWORK_ERROR_DELAY_MS) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/GzipRequestInterceptorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/GzipRequestInterceptorTest.kt new file mode 100644 index 0000000000..1a42473440 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/GzipRequestInterceptorTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Interceptor +import okhttp3.MultipartBody +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okio.Buffer +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.ByteArrayOutputStream + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class GzipRequestInterceptorTest { + lateinit var testedInterceptor: Interceptor + + @Mock + lateinit var mockChain: Interceptor.Chain + + @Mock + lateinit var mockInternalLogger: InternalLogger + + lateinit var fakeRequest: Request + lateinit var fakeResponse: Response + lateinit var fakeBody: String + + @BeforeEach + fun `set up`(forge: Forge) { + val fakeUrl = forge.aStringMatching("http://[a-z0-9_]{8}\\.[a-z]{3}") + fakeBody = forge.anAlphabeticalString() + fakeRequest = Request.Builder() + .url(http://github.com/fakeUrl) + .post(fakeBody.toByteArray().toRequestBody(null)) + .build() + testedInterceptor = GzipRequestInterceptor(mockInternalLogger) + } + + @Test + fun `compress body when no encoding is used`() { + fakeResponse = forgeResponse() + stubChain() + + val response = testedInterceptor.intercept(mockChain) + + argumentCaptor { + verify(mockChain).proceed(capture()) + val buffer = Buffer() + val stream = ByteArrayOutputStream() + lastValue.body!!.writeTo(buffer) + buffer.copyTo(stream) + + assertThat(stream.toString()) + .isNotEqualTo(fakeBody) + + assertThat(lastValue.header("Content-Encoding")) + .isEqualTo("gzip") + } + assertThat(response) + .isSameAs(fakeResponse) + } + + @Test + fun `ignores body when encoding is set`() { + fakeRequest = fakeRequest.newBuilder() + .header("Content-Encoding", "identity") + .build() + fakeResponse = forgeResponse() + stubChain() + + val response = testedInterceptor.intercept(mockChain) + + argumentCaptor { + verify(mockChain).proceed(capture()) + val buffer = Buffer() + val stream = ByteArrayOutputStream() + lastValue.body!!.writeTo(buffer) + buffer.copyTo(stream) + + assertThat(stream.toString()) + .isEqualTo(fakeBody) + assertThat(lastValue.header("Content-Encoding")) + .isEqualTo("identity") + } + assertThat(response) + .isSameAs(fakeResponse) + } + + @Test + fun `M keep original body W intercept { MultipartBody }`(forge: Forge) { + // Given + val fakeMultipartBody = MultipartBody + .Builder() + .addFormDataPart( + forge.aString(), + forge.aString(), + fakeBody.toByteArray().toRequestBody(null) + ) + .build() + + fakeRequest = fakeRequest.newBuilder() + .post(fakeMultipartBody) + .build() + fakeResponse = forgeResponse() + stubChain() + + // When + val response = testedInterceptor.intercept(mockChain) + + // Then + argumentCaptor { + verify(mockChain).proceed(capture()) + val buffer = Buffer() + val stream = ByteArrayOutputStream() + val part = (lastValue.body as MultipartBody).part(0) + part.body.writeTo(buffer) + buffer.copyTo(stream) + + assertThat(stream.toString()) + .isEqualTo(fakeBody) + } + assertThat(response) + .isSameAs(fakeResponse) + } + + @Test + fun `ignores body when body is null`() { + fakeRequest = fakeRequest.newBuilder() + .get() + .build() + fakeResponse = forgeResponse() + stubChain() + + val response = testedInterceptor.intercept(mockChain) + + argumentCaptor { + verify(mockChain).proceed(capture()) + assertThat(lastValue.body) + .isNull() + assertThat(lastValue.header("Content-Encoding")) + .isNull() + } + assertThat(response) + .isSameAs(fakeResponse) + } + + // region Internal + + private fun forgeResponse(): Response { + val builder = Response.Builder() + .request(fakeRequest) + .protocol(Protocol.HTTP_2) + .code(200) + .message("{}") + return builder.build() + } + + private fun stubChain() { + whenever(mockChain.request()) doReturn fakeRequest + whenever(mockChain.proceed(any())) doReturn fakeResponse + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/RotatingDnsResolverTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/RotatingDnsResolverTest.kt new file mode 100644 index 0000000000..d25b9fe7b7 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/RotatingDnsResolverTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Dns +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.net.InetAddress +import kotlin.time.Duration.Companion.milliseconds + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class RotatingDnsResolverTest { + + lateinit var testedDns: Dns + + @Mock + lateinit var mockDelegate: Dns + + @StringForgery + lateinit var fakeHostname: String + + lateinit var fakeInetAddresses: List + + @BeforeEach + fun `set up`(forge: Forge) { + fakeInetAddresses = forge.aList { mock() } + + testedDns = RotatingDnsResolver(mockDelegate, TEST_TTL_MS) + } + + @Test + fun `M return delegate result W lookup {unknown hostname}`() { + // Given + whenever(mockDelegate.lookup(fakeHostname)) doReturn fakeInetAddresses + + // When + val result = testedDns.lookup(fakeHostname) + + // Then + assertThat(result).containsExactlyElementsOf(fakeInetAddresses) + } + + @Test + fun `M rotate known result W lookup {known hostname}`() { + // Given + whenever(mockDelegate.lookup(fakeHostname)) doReturn fakeInetAddresses + val result = mutableListOf() + + // When + fakeInetAddresses.forEach { + result.add(testedDns.lookup(fakeHostname).first()) + } + + // Then + assertThat(result).containsExactlyElementsOf(fakeInetAddresses) + } + + @Test + fun `M renew result W lookup {expired hostname}`( + forge: Forge + ) { + // Given + val fakeInetAddresses2: List = forge.aList { mock() } + whenever(mockDelegate.lookup(fakeHostname)).doReturn(fakeInetAddresses, fakeInetAddresses2) + + // When + val result = testedDns.lookup(fakeHostname) + Thread.sleep(TEST_TTL_MS.inWholeMilliseconds) + val result2 = testedDns.lookup(fakeHostname) + + // Then + assertThat(result).containsExactlyElementsOf(fakeInetAddresses) + assertThat(result2).containsExactlyElementsOf(fakeInetAddresses2) + } + + @Test + fun `M renew result W lookup {empty hostname}`() { + // Given + whenever(mockDelegate.lookup(fakeHostname)).doReturn(emptyList(), fakeInetAddresses) + + // When + val result = testedDns.lookup(fakeHostname) + val result2 = testedDns.lookup(fakeHostname) + + // Then + assertThat(result).isEmpty() + assertThat(result2).containsExactlyElementsOf(fakeInetAddresses) + } + + @RepeatedTest(30) + fun `M not throw exception W concurrent access to lookup`(forge: Forge) { + // Given + // we need to keep the list of addresses low as it can only be reproduced with low number and it reflects + // the real use case where we have a small number of addresses to rotate + fakeInetAddresses = forge.aList(size = forge.anInt(min = 1, max = 3)) { mock() } + whenever(mockDelegate.lookup(fakeHostname)) doReturn fakeInetAddresses + // just wait the TTL time to make sure all threads are concurrently accessing the lookup + Thread.sleep(TEST_TTL_MS.inWholeMilliseconds) + var exceptionThrown: Exception? = null + + // When + List(100) { + Thread { + Thread.sleep(forge.aLong(min = 0, max = 100)) + try { + testedDns.lookup(fakeHostname) + } catch (e: Exception) { + exceptionThrown = e + } + }.apply { + start() + } + }.forEach { it.join() } + + // Then + assertThat(exceptionThrown).isNull() + } + + companion object { + internal val TEST_TTL_MS = 250.milliseconds + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadStatusTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadStatusTest.kt new file mode 100644 index 0000000000..f3d5ba5bda --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadStatusTest.kt @@ -0,0 +1,618 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.times +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class UploadStatusTest { + + @StringForgery + lateinit var fakeContext: String + + @Mock + lateinit var mockLogger: InternalLogger + + @IntForgery(min = 0) + var fakeByteSize: Int = 0 + + @StringForgery(StringForgeryType.HEXADECIMAL) + lateinit var fakeRequestId: String + + @IntForgery + var fakeRequestAttempts: Int = 0 + + @Test + fun `M log SUCCESS only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.Success + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.INFO, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) sent successfully." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log SUCCESS only to USER W logStatus()`( + @Forgery status: UploadStatus.Success + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.INFO, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) sent successfully." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log NETWORK_ERROR only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.NetworkError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a network error (${throwable.javaClass.name}: ${throwable.message}); " + + "we will retry later. This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log NETWORK_ERROR only to USER W logStatus()`( + @Forgery status: UploadStatus.NetworkError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a network error (${throwable.javaClass.name}: ${throwable.message}); " + + "we will retry later. This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log DNS_ERROR only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.DNSError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a DNS error (${throwable.javaClass.name}: ${throwable.message}); " + + "we will retry later. This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log DNS_ERROR only to USER W logStatus()`( + @Forgery status: UploadStatus.DNSError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a DNS error (${throwable.javaClass.name}: ${throwable.message}); " + + "we will retry later. This request was attempted $fakeRequestAttempts time(s)." + + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log INVALID_TOKEN_ERROR only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.InvalidTokenError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because your token is invalid; the batch was dropped. " + + "Make sure that the provided token still " + + "exists and you're targeting the relevant Datadog site." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log INVALID_TOKEN_ERROR only to USER W logStatus()`( + @Forgery status: UploadStatus.InvalidTokenError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because your token is invalid; the batch was dropped. " + + "Make sure that the provided token still " + + "exists and you're targeting the relevant Datadog site." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log HTTP_REDIRECTION only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.HttpRedirection + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a network redirection; the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log HTTP_REDIRECTION only to USER W logStatus()`( + @Forgery status: UploadStatus.HttpRedirection + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a network redirection; the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log HTTP_CLIENT_ERROR to USER and TELEMETRY W logStatus() {no request id}`( + @Forgery status: UploadStatus.HttpClientError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a processing error or invalid data; " + + "the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log HTTP_CLIENT_ERROR to USER and TELEMETRY W logStatus()`( + @Forgery status: UploadStatus.HttpClientError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a processing error or invalid data; " + + "the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log HTTP_CLIENT_ERROR_RATE_LIMITING to USER and TELEMETRY W logStatus() {no request id}`( + @Forgery status: UploadStatus.HttpClientRateLimiting + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Batch [$fakeByteSize bytes] ($fakeContext) failed because of an intake rate limitation; " + + "we will retry later." + + " This request was attempted $fakeRequestAttempts time(s)." + + ) + } + + @Test + fun `M log HTTP_CLIENT_ERROR_RATE_LIMITING to USER and TELEMETRY W logStatus()`( + @Forgery status: UploadStatus.HttpClientRateLimiting + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed because of an intake rate limitation; " + + "we will retry later." + + " This request was attempted $fakeRequestAttempts time(s)." + + ) + } + + @Test + fun `M log HTTP_SERVER_ERROR only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.HttpServerError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a server processing error; we will retry later." + + " This request was attempted $fakeRequestAttempts time(s)." + + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log HTTP_SERVER_ERROR only to USER W logStatus()`( + @Forgery status: UploadStatus.HttpServerError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of a server processing error; we will retry later." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + verifyNoMoreInteractions(mockLogger) + } + + @Test + fun `M log UNKNOWN_HTTP_ERROR only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.UnknownHttpError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of an unexpected HTTP error (status code = ${status.code}); the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + } + + @Test + fun `M log UNKNOWN_HTTP_ERROR only to USER W logStatus()`( + @Forgery status: UploadStatus.UnknownHttpError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of an unexpected HTTP error (status code = ${status.code}); the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + } + + @Test + fun `M log UNKNOWN_EXCEPTION only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.UnknownException + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of an unknown error (${throwable.javaClass.name}: ${throwable.message}); " + + "we will retry later. This request was attempted $fakeRequestAttempts time(s)." + ) + } + + @Test + fun `M log UNKNOWN_EXCEPTION only to USER W logStatus()`( + @Forgery status: UploadStatus.UnknownException + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of an unknown error (${throwable.javaClass.name}: ${throwable.message}); " + + "we will retry later. This request was attempted $fakeRequestAttempts time(s)." + ) + } + + @Test + fun `M log INVALID_REQUEST_ERROR only to USER W logStatus() {no request id}`( + @Forgery status: UploadStatus.RequestCreationError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch [$fakeByteSize bytes] ($fakeContext) failed " + + "because of an error when creating the request (${throwable.javaClass.name}: ${throwable.message}); " + + "the batch was dropped. This request was attempted $fakeRequestAttempts time(s)." + ) + } + + @Test + fun `M log INVALID_REQUEST_ERROR only to USER W logStatus()`( + @Forgery status: UploadStatus.RequestCreationError + ) { + // When + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + val throwable = status.throwable!! + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) failed " + + "because of an error when creating the request (${throwable.javaClass.name}: ${throwable.message}); " + + "the batch was dropped. This request was attempted $fakeRequestAttempts time(s)." + ) + } + + @Test + fun `M log UNKNOWN_STATUS only to USER W logStatus() {no request id}`() { + // When + val status = UploadStatus.UnknownStatus + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Batch [$fakeByteSize bytes] ($fakeContext) status is unknown;" + + " the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + } + + @Test + fun `M log UNKNOWN_STATUS only to USER W logStatus()`() { + // When + val status = UploadStatus.UnknownStatus + status.logStatus( + fakeContext, + fakeByteSize, + mockLogger, + attempts = fakeRequestAttempts, + requestId = fakeRequestId + ) + + // Then + mockLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + "Batch $fakeRequestId [$fakeByteSize bytes] ($fakeContext) status is unknown;" + + " the batch was dropped." + + " This request was attempted $fakeRequestAttempts time(s)." + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadWorkerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadWorkerTest.kt new file mode 100644 index 0000000000..0196dda402 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadWorkerTest.kt @@ -0,0 +1,967 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.data.upload + +import android.content.Context +import androidx.work.Data +import androidx.work.ListenableWorker +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.datadog.android.Datadog +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.UploadWorker +import com.datadog.android.core.internal.NoOpInternalSdkCore +import com.datadog.android.core.internal.SdkFeature +import com.datadog.android.core.internal.data.upload.UploadStatus.Companion.UNKNOWN_RESPONSE_CODE +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.persistence.BatchData +import com.datadog.android.core.internal.persistence.BatchId +import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.config.InternalLoggerTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.invocation.InvocationOnMock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import org.mockito.stubbing.Answer + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class UploadWorkerTest { + + private lateinit var testedWorker: Worker + + @Mock + lateinit var mockSdkCore: InternalSdkCore + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @StringForgery + lateinit var fakeInstanceName: String + + @Forgery + lateinit var fakeWorkerParameters: WorkerParameters + + var fakeFeaturesCount: Int = 0 + + lateinit var mockFeatures: List + + lateinit var mockUploaders: List + + lateinit var mockStorages: List + + lateinit var fakeFeatureBatches: List>> + + lateinit var fakeFeatureBatchIds: List> + + lateinit var fakeFeatureBatchMetadata: List> + + @BeforeEach + fun `set up`(forge: Forge) { + whenever(mockSdkCore.getDatadogContext()) doReturn fakeDatadogContext + Datadog.registry.register(fakeInstanceName, mockSdkCore) + + val fakeData = Data.Builder() + .putString(UploadWorker.DATADOG_INSTANCE_NAME, fakeInstanceName) + .build() + fakeWorkerParameters = fakeWorkerParameters.copyWith(fakeData) + + fakeFeaturesCount = forge.anInt(2, 8) + createFakeBatches(forge) + stubFeaturesStorage() + + testedWorker = UploadWorker( + appContext.mockInstance, + fakeWorkerParameters + ) + } + + @AfterEach + fun `tear down`() { + Datadog.registry.clear() + } + + // region setup + + private fun createFakeBatches(forge: Forge) { + fakeFeatureBatches = List(fakeFeaturesCount) { + forge.aList { forge.aList { forge.getForgery() } } + } + + fakeFeatureBatchIds = List(fakeFeaturesCount) { featureIndex -> + forge.aList(fakeFeatureBatches[featureIndex].size) { BatchId(forge.aString()) } + } + + fakeFeatureBatchMetadata = List(fakeFeaturesCount) { featureIndex -> + forge.aList(fakeFeatureBatches[featureIndex].size) { forge.aNullable { aString().toByteArray() } } + } + } + + private fun stubFeaturesStorage() { + mockFeatures = List(fakeFeaturesCount) { mock() } + mockUploaders = List(fakeFeaturesCount) { mock() } + mockStorages = List(fakeFeaturesCount) { mock() } + + whenever(mockSdkCore.getAllFeatures()) doReturn mockFeatures + + mockFeatures.forEachIndexed { featureIndex, feature -> + whenever(feature.uploader) doReturn mockUploaders[featureIndex] + whenever(feature.storage) doReturn mockStorages[featureIndex] + + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + val fakeBatchMetadata = fakeFeatureBatchMetadata[featureIndex] + + val batchesCount = fakeBatches.size + whenever(mockStorages[featureIndex].readNextBatch()) + .thenAnswer(object : Answer { + var invocationCount: Int = 0 + + override fun answer(invocation: InvocationOnMock): BatchData? { + if (invocationCount >= batchesCount) { + return null + } + val fakeBatch = fakeBatches[invocationCount] + val fakeBatchId = fakeBatchIds[invocationCount] + val fakeMetadata = fakeBatchMetadata[invocationCount] + invocationCount++ + + return BatchData( + id = fakeBatchId, + data = fakeBatch, + metadata = fakeMetadata + ) + } + }) + } + } + + private fun stubFeaturesUploaders( + successStatusCode: Int = 202, + successfulUntilIdx: Int = Int.MAX_VALUE, + secondaryStatus: UploadStatus = UploadStatus.UnknownStatus + ) { + mockFeatures.forEachIndexed { featureIndex, _ -> + val mockUploader = mockUploaders[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeBatchMetadata = fakeFeatureBatchMetadata[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, batch -> + val status = if (batchIndex < successfulUntilIdx) { + UploadStatus.Success(successStatusCode) + } else { + secondaryStatus + } + whenever( + mockUploader.upload( + fakeDatadogContext, + batch, + fakeBatchMetadata[batchIndex], + fakeFeatureBatchIds[featureIndex][batchIndex] + ) + ) doReturn status + } + } + } + + // endregion + + // region doWork + + @Test + fun `M do nothing W doWork() {no sdk}`() { + // Given + Datadog.registry.unregister(fakeInstanceName) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + mockUploaders.forEach { verifyNoInteractions(it) } + mockStorages.forEach { verifyNoInteractions(it) } + } + + @Test + fun `M do nothing W doWork() {no op sdk}`() { + // Given + Datadog.registry.unregister(fakeInstanceName) + Datadog.registry.register(fakeInstanceName, NoOpInternalSdkCore) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + mockUploaders.forEach { verifyNoInteractions(it) } + mockStorages.forEach { verifyNoInteractions(it) } + } + + @Test + fun `M send all batches W doWork() {all success}`( + @IntForgery(200, 300) successStatusCode: Int + ) { + // Given + stubFeaturesUploaders(successStatusCode) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + } + } + + @Test + fun `M send all batches until failure W doWork() {unauthorized}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @IntForgery(400, 499) failureStatusCode: Int + + ) { + // Given + val failingStatus = UploadStatus.InvalidTokenError(failureStatusCode) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(failureStatusCode), + deleteBatch = true + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {rate limiting}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @IntForgery(400, 499) failureStatusCode: Int + ) { + // Given + val failingStatus = UploadStatus.HttpClientRateLimiting(failureStatusCode) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(failureStatusCode), + deleteBatch = false + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {client error}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @IntForgery(400, 499) failureStatusCode: Int + ) { + // Given + val failingStatus = UploadStatus.HttpClientError(failureStatusCode) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(failureStatusCode), + deleteBatch = true + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {server error}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @IntForgery(400, 499) failureStatusCode: Int + ) { + // Given + val failingStatus = UploadStatus.HttpServerError(failureStatusCode) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(failureStatusCode), + deleteBatch = false + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {redirection}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @IntForgery(300, 399) failureStatusCode: Int + ) { + // Given + val failingStatus = UploadStatus.HttpRedirection(failureStatusCode) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(failureStatusCode), + deleteBatch = true + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {unknown http error}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @IntForgery(400, 499) failureStatusCode: Int + ) { + // Given + val failingStatus = UploadStatus.UnknownHttpError(failureStatusCode) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(failureStatusCode), + deleteBatch = true + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {network error}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @Forgery throwable: Exception + ) { + // Given + val failingStatus = UploadStatus.NetworkError(throwable) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(UNKNOWN_RESPONSE_CODE), + deleteBatch = false + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {dns error}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @Forgery throwable: Exception + ) { + // Given + val failingStatus = UploadStatus.DNSError(throwable) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(UNKNOWN_RESPONSE_CODE), + deleteBatch = false + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {request creation error}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @Forgery throwable: Exception + ) { + // Given + val failingStatus = UploadStatus.RequestCreationError(throwable) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(UNKNOWN_RESPONSE_CODE), + deleteBatch = true + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {unknown exception}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int, + @Forgery throwable: Exception + ) { + // Given + val failingStatus = UploadStatus.UnknownException(throwable) + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(UNKNOWN_RESPONSE_CODE), + deleteBatch = false + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + @Test + fun `M send all batches until failure W doWork() {unknown status}`( + @IntForgery(200, 300) successStatusCode: Int, + @IntForgery(0, 8) successfulBatchCount: Int + ) { + // Given + val failingStatus = UploadStatus.UnknownStatus + stubFeaturesUploaders(successStatusCode, successfulBatchCount, failingStatus) + + // When + val result = testedWorker.doWork() + + // Then + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + repeat(fakeFeaturesCount) { featureIndex -> + val mockUploader = mockUploaders[featureIndex] + val mockStorage = mockStorages[featureIndex] + val fakeBatches = fakeFeatureBatches[featureIndex] + val fakeMetadata = fakeFeatureBatchMetadata[featureIndex] + val fakeBatchIds = fakeFeatureBatchIds[featureIndex] + + fakeBatches.forEachIndexed { batchIndex, fakeBatch -> + // n successful batches + if (batchIndex < successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(successStatusCode), + deleteBatch = true + ) + } + + // failing batch + if (batchIndex == successfulBatchCount) { + verify(mockUploader).upload( + context = fakeDatadogContext, + batch = fakeBatch, + batchMeta = fakeMetadata[batchIndex], + batchId = fakeFeatureBatchIds[featureIndex][batchIndex] + ) + verify(mockStorage).confirmBatchRead( + batchId = fakeBatchIds[batchIndex], + removalReason = RemovalReason.IntakeCode(UNKNOWN_RESPONSE_CODE), + deleteBatch = true + ) + } + } + + verifyNoMoreInteractions(mockUploader) + } + } + + // endregion + + // region Internal + + private fun WorkerParameters.copyWith( + inputData: Data + ): WorkerParameters { + return WorkerParameters( + id, + inputData, + tags, + runtimeExtras, + runAttemptCount, + generation, + backgroundExecutor, + taskExecutor, + workerFactory, + progressUpdater, + foregroundUpdater + ) + } + + // endregion + + companion object { + val logger = InternalLoggerTestConfiguration() + val appContext = ApplicationContextTestConfiguration(Context::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext, logger) + } + + @JvmStatic + fun errorWithRetryStatusValues(): List { + val forge = Forge().apply { + Configurator().configure(this) + } + + return listOf( + forge.getForgery(UploadStatus.Success::class.java), + + forge.getForgery(UploadStatus.InvalidTokenError::class.java), + forge.getForgery(UploadStatus.HttpClientRateLimiting::class.java), + forge.getForgery(UploadStatus.HttpClientError::class.java), + forge.getForgery(UploadStatus.HttpServerError::class.java), + forge.getForgery(UploadStatus.HttpRedirection::class.java), + forge.getForgery(UploadStatus.UnknownHttpError::class.java), + + forge.getForgery(UploadStatus.NetworkError::class.java), + forge.getForgery(UploadStatus.DNSError::class.java), + forge.getForgery(UploadStatus.RequestCreationError::class.java), + + forge.getForgery(UploadStatus.UnknownException::class.java), + forge.getForgery(UploadStatus.UnknownStatus::class.java) + ) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallbackTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallbackTest.kt new file mode 100644 index 0000000000..0250de0afd --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallbackTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.lifecycle + +import android.content.Context +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.impl.WorkManagerImpl +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.UploadWorker +import com.datadog.android.core.internal.utils.TAG_DATADOG_UPLOAD +import com.datadog.android.core.internal.utils.UPLOAD_WORKER_NAME +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.datadog.tools.unit.setStaticValue +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ProcessLifecycleCallbackTest { + + lateinit var testedCallback: ProcessLifecycleCallback + + @Mock + lateinit var mockWorkManager: WorkManagerImpl + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @StringForgery + lateinit var fakeInstanceName: String + + @BeforeEach + fun `set up`() { + testedCallback = ProcessLifecycleCallback(appContext.mockInstance, fakeInstanceName, mockInternalLogger) + } + + @AfterEach + fun `tear down`() { + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", null) + } + + @Test + fun `when process stopped will schedule an upload worker`() { + // Given + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) + whenever( + mockWorkManager.enqueueUniqueWork( + ArgumentMatchers.anyString(), + any(), + any() + ) + ) doReturn mock() + + // When + testedCallback.onStopped() + + // Then + argumentCaptor { + verify(mockWorkManager).enqueueUniqueWork( + eq(UPLOAD_WORKER_NAME), + eq(ExistingWorkPolicy.REPLACE), + capture() + ) + val workSpec = lastValue.workSpec + assertThat(workSpec.workerClassName).isEqualTo(UploadWorker::class.java.canonicalName) + assertThat(workSpec.input.getString(UploadWorker.DATADOG_INSTANCE_NAME)).isEqualTo(fakeInstanceName) + assertThat(lastValue.tags).contains("$TAG_DATADOG_UPLOAD/$fakeInstanceName") + } + } + + @Test + fun `when process stopped and work manager is not present will not throw exception`() { + // When + testedCallback.onStopped() + + // Then + verifyNoInteractions(mockWorkManager) + } + + @Test + fun `when process stopped and context ref is null will do nothing`() { + testedCallback.contextWeakRef.clear() + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) + + // When + testedCallback.onStopped() + + // Then + verifyNoInteractions(mockWorkManager) + } + + @Test + fun `when process started cancel existing workers`() { + // Given + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) + + // When + testedCallback.onStarted() + + // Then + verify(mockWorkManager).cancelAllWorkByTag("$TAG_DATADOG_UPLOAD/$fakeInstanceName") + } + + @Test + fun `when process started do nothing if no work manager`() { + // Given + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", null) + + // When + testedCallback.onStarted() + + // Then + verifyNoInteractions(mockWorkManager) + } + + companion object { + val appContext = ApplicationContextTestConfiguration(Context::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext) + } + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitorTest.kt similarity index 95% rename from dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitorTest.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitorTest.kt index 2a7fbad492..2c6966edf0 100644 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitorTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleMonitorTest.kt @@ -7,13 +7,6 @@ package com.datadog.android.core.internal.lifecycle import android.app.Activity -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import java.util.concurrent.CountDownLatch import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -21,7 +14,14 @@ import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch @Extensions( ExtendWith(MockitoExtension::class) @@ -165,7 +165,7 @@ internal class ProcessLifecycleMonitorTest { testedMonitor.onActivityStopped(mockActivity1) // Then - verifyZeroInteractions(mockCallback) + verifyNoInteractions(mockCallback) } @Test @@ -186,7 +186,6 @@ internal class ProcessLifecycleMonitorTest { @Test fun `when starting activities from 2 different threads will only call onResumed once`() { - // Given val countDownLatch = CountDownLatch(2) @@ -212,7 +211,6 @@ internal class ProcessLifecycleMonitorTest { @Test fun `when stopped from 2 different threads will only call onStooped once`() { - // Given testedMonitor.onActivityStarted(mockActivity1) testedMonitor.onActivityResumed(mockActivity1) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerTest.kt new file mode 100644 index 0000000000..9f3894f102 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerTest.kt @@ -0,0 +1,827 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.logger + +import android.util.Log +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry +import com.datadog.android.core.metrics.TelemetryMetricType +import com.datadog.android.internal.attributes.LocalAttribute +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.aThrowable +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset.offset +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.mockingDetails +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class SdkInternalLoggerTest { + + @Mock + lateinit var mockUserLogHandler: LogcatLogHandler + + @Mock + lateinit var mockMaintainerLogHandler: LogcatLogHandler + + @Mock + lateinit var mockRumFeatureScope: FeatureScope + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) + lateinit var fakeInstanceName: String + + private lateinit var testedInternalLogger: SdkInternalLogger + + @BeforeEach + fun `set up`() { + whenever(mockSdkCore.name) doReturn fakeInstanceName + + testedInternalLogger = SdkInternalLogger( + sdkCore = mockSdkCore, + userLogHandlerFactory = { mockUserLogHandler }, + maintainerLogHandlerFactory = { mockMaintainerLogHandler } + ) + + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope + } + + fun callWithLambda(i: Int, lambda: () -> String) { + if (i == 0) { + print(lambda()) + } + } + + fun callWithString(i: Int, str: String) { + if (i == 0) { + print(str) + } + } + + // region Target.USER + + @Test + fun `M send user log W log { USER target }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.aValueFrom(InternalLogger.Level::class.java) + val fakeThrowable = forge.aNullable { forge.aThrowable() } + whenever(mockUserLogHandler.canLog(any())) doReturn true + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.USER, + mockLambda, + fakeThrowable + ) + + // Then + verify(mockUserLogHandler) + .log( + fakeLevel.toLogLevel(), + "[$fakeInstanceName]: $fakeMessage", + fakeThrowable + ) + } + + @Test + fun `M send user log only once W log { USER target, onlyOnce=true }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.aValueFrom(InternalLogger.Level::class.java) + val fakeThrowable = forge.aNullable { forge.aThrowable() } + whenever(mockUserLogHandler.canLog(any())) doReturn true + + // When + repeat(10) { + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.USER, + mockLambda, + fakeThrowable, + true + ) + } + + // Then + verify(mockUserLogHandler) + .log( + fakeLevel.toLogLevel(), + "[$fakeInstanceName]: $fakeMessage", + fakeThrowable + ) + } + + @Test + fun `M send user log with condition W log { USER target }`( + @IntForgery(min = Log.VERBOSE, max = (Log.ASSERT + 1)) sdkVerbosity: Int + ) { + // Given + Datadog.setVerbosity(sdkVerbosity) + + // When + testedInternalLogger = SdkInternalLogger( + sdkCore = mockSdkCore, + maintainerLogHandlerFactory = { mockMaintainerLogHandler } + ) + + // Then + val predicate = testedInternalLogger.userLogger.predicate + for (i in 0..10) { + if (i >= sdkVerbosity) { + assertThat(predicate(i)).isTrue + } else { + assertThat(predicate(i)).isFalse + } + } + } + + @Test + fun `M not evaluate lambda W log { USER target }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.aValueFrom(InternalLogger.Level::class.java) + val fakeThrowable = forge.aNullable { forge.aThrowable() } + whenever(mockUserLogHandler.canLog(any())) doReturn false + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.USER, + mockLambda, + fakeThrowable, + true + ) + + // Then + verify(mockLambda, never()).invoke() + } + + // endregion + + @Test + fun `M send maintainer log W log { MAINTAINER target }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.aValueFrom(InternalLogger.Level::class.java) + val fakeThrowable = forge.aNullable { forge.aThrowable() } + whenever(mockMaintainerLogHandler.canLog(any())) doReturn true + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.MAINTAINER, + mockLambda, + fakeThrowable + ) + + // Then + verify(mockMaintainerLogHandler).log( + fakeLevel.toLogLevel(), + "[$fakeInstanceName]: $fakeMessage", + fakeThrowable + ) + } + + @Test + fun `M send maintainer log only once W log { MAINTAINER target, onlyOnce=true }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.aValueFrom(InternalLogger.Level::class.java) + val fakeThrowable = forge.aNullable { forge.aThrowable() } + whenever(mockMaintainerLogHandler.canLog(any())) doReturn true + + // When + repeat(10) { + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.MAINTAINER, + mockLambda, + fakeThrowable, + true + ) + } + + // Then + verify(mockMaintainerLogHandler).log( + fakeLevel.toLogLevel(), + "[$fakeInstanceName]: $fakeMessage", + fakeThrowable + ) + } + + @Test + fun `M send telemetry log W log { TELEMETRY target, no throwable + info or debug }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.anElementFrom(InternalLogger.Level.INFO, InternalLogger.Level.DEBUG) + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.TELEMETRY, + mockLambda, + null + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Debug + assertThat(logEvent.message).isEqualTo(fakeMessage) + assertThat(logEvent.additionalProperties).isNull() + } + } + + @Test + fun `M send telemetry log W log { TELEMETRY target, additional properties + info or debug }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val fakeAdditionalProperties = forge.aMap { + forge.anAlphabeticalString() to forge.aNullable { anAlphabeticalString() } + } + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.anElementFrom(InternalLogger.Level.INFO, InternalLogger.Level.DEBUG) + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.TELEMETRY, + mockLambda, + null, + additionalProperties = fakeAdditionalProperties + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Debug + assertThat(logEvent.message).isEqualTo(fakeMessage) + assertThat(logEvent.additionalProperties).isEqualTo(fakeAdditionalProperties) + } + } + + @Test + fun `M send telemetry log W log { TELEMETRY target, additional prop empty + info or debug }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.anElementFrom(InternalLogger.Level.INFO, InternalLogger.Level.DEBUG) + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.TELEMETRY, + mockLambda, + null, + additionalProperties = emptyMap() + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Debug + assertThat(logEvent.message).isEqualTo(fakeMessage) + assertThat(logEvent.additionalProperties).isEmpty() + } + } + + @Test + fun `M send telemetry log W log { TELEMETRY target, no throwable + warn or error }`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val fakeAdditionalProperties = forge.exhaustiveAttributes() + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.anElementFrom(InternalLogger.Level.WARN, InternalLogger.Level.ERROR) + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.TELEMETRY, + mockLambda, + null, + additionalProperties = fakeAdditionalProperties + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Error + assertThat(logEvent.message).isEqualTo(fakeMessage) + assertThat(logEvent.additionalProperties).isEqualTo(fakeAdditionalProperties) + } + } + + @Test + fun `M send telemetry log W log { TELEMETRY target, with throwable}`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val fakeAdditionalProperties = forge.exhaustiveAttributes() + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.aValueFrom(InternalLogger.Level::class.java) + val fakeThrowable = forge.aThrowable() + + // When + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.TELEMETRY, + mockLambda, + fakeThrowable, + additionalProperties = fakeAdditionalProperties + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Error + assertThat(logEvent.message).isEqualTo(fakeMessage) + assertThat(logEvent.error).isEqualTo(fakeThrowable) + assertThat(logEvent.additionalProperties).isEqualTo(fakeAdditionalProperties) + } + } + + @Test + fun `M send telemetry log only once W log { TELEMETRY target, onlyOnce=true}`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val fakeLevel = forge.anElementFrom(InternalLogger.Level.INFO, InternalLogger.Level.DEBUG) + + // When + repeat(10) { + testedInternalLogger.log( + fakeLevel, + InternalLogger.Target.TELEMETRY, + mockLambda, + null, + true + ) + } + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val logEvent = firstValue as InternalTelemetryEvent.Log.Debug + assertThat(logEvent.message).isEqualTo(fakeMessage) + } + } + + @Test + fun `M send metric W metric() {sampling 100 percent}`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val samplingRate = 100.0f + val fakeAdditionalProperties = forge.exhaustiveAttributes().also { + it[LocalAttribute.Key.REPORTING_SAMPLING_RATE.toString()] = samplingRate + } + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + + // When + testedInternalLogger.logMetric( + mockLambda, + fakeAdditionalProperties, + samplingRate + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val metricEvent = firstValue as InternalTelemetryEvent.Metric + assertThat(metricEvent.message).isEqualTo(fakeMessage) + assertThat(metricEvent.additionalProperties).isEqualTo(fakeAdditionalProperties) + } + } + + @ParameterizedTest + @ValueSource(floats = [0.0f, 0.3f]) + fun `M creationSampleRate is sent if present W logMetric() {sampling 100 percent}`( + creationSampleRate: Float, + forge: Forge + ) { + // Given + val mockLambda: () -> String = mock { + on { invoke() } doReturn forge.aString() + } + whenever(mockLambda.invoke()) doReturn forge.aString() + + // When + val samplingRate = 100.0f + val expectedCreationSampleRate = creationSampleRate.takeIf { + it > 0.0f // ValueSource doesn't allows to use null values + } + + testedInternalLogger.logMetric( + mockLambda, + emptyMap(), + samplingRate, + expectedCreationSampleRate + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val metricEvent = firstValue as InternalTelemetryEvent.Metric + + assertThat( + metricEvent.additionalProperties?.get(LocalAttribute.Key.CREATION_SAMPLING_RATE.toString()) + ).isEqualTo( + expectedCreationSampleRate + ) + + assertThat( + metricEvent.additionalProperties?.get(LocalAttribute.Key.REPORTING_SAMPLING_RATE.toString()) + ).isEqualTo( + samplingRate + ) + } + } + + @Test + fun `M creationSampleRate is sent if present W logApiUsage() {sampling 100 percent}`( + forge: Forge + ) { + // Given + val mockLambda: () -> InternalTelemetryEvent.ApiUsage = mock { + on { invoke() } doReturn forge.getForgery() + } + + // When + val samplingRate = 100f + + testedInternalLogger.logApiUsage( + samplingRate, + mockLambda + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + + val apiUsageEvent = firstValue as InternalTelemetryEvent.ApiUsage + + assertThat( + apiUsageEvent.additionalProperties + ).doesNotContainKeys( + LocalAttribute.Key.CREATION_SAMPLING_RATE.toString() + ) + + assertThat( + apiUsageEvent.additionalProperties[LocalAttribute.Key.REPORTING_SAMPLING_RATE.toString()] + ).isEqualTo( + samplingRate + ) + } + } + + @Test + fun `M creationSampleRate is sent if present W log() {debug event}`( + forge: Forge + ) { + // When + val samplingRate = forge.aFloat(min = .1f, max = 100f) + + testedInternalLogger.log( + level = forge.aValueFrom( + InternalLogger.Level::class.java, + exclude = listOf(InternalLogger.Level.WARN, InternalLogger.Level.ERROR) + ), + target = InternalLogger.Target.TELEMETRY, + messageBuilder = { forge.aString() }, + additionalProperties = mapOf(LocalAttribute.Key.REPORTING_SAMPLING_RATE.toString() to samplingRate) + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + + val debugEvent = firstValue as InternalTelemetryEvent.Log.Debug + + assertThat( + debugEvent.additionalProperties + ).doesNotContainKeys( + LocalAttribute.Key.CREATION_SAMPLING_RATE.toString() + ) + + assertThat( + debugEvent.additionalProperties?.get(LocalAttribute.Key.REPORTING_SAMPLING_RATE.toString()) + ).isEqualTo( + samplingRate + ) + } + } + + @Test + fun `M creationSampleRate is sent if present W log() {error event}`( + forge: Forge + ) { + // When + val samplingRate = forge.aFloat(min = .1f, max = 100f) + + testedInternalLogger.log( + level = forge.anElementFrom(InternalLogger.Level.WARN, InternalLogger.Level.ERROR), + target = InternalLogger.Target.TELEMETRY, + throwable = forge.aThrowable(), + messageBuilder = { forge.aString() }, + additionalProperties = mapOf(LocalAttribute.Key.REPORTING_SAMPLING_RATE.toString() to samplingRate) + ) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + + val debugEvent = firstValue as InternalTelemetryEvent.Log.Error + + assertThat( + debugEvent.additionalProperties + ).doesNotContainKeys( + LocalAttribute.Key.CREATION_SAMPLING_RATE.toString() + ) + + assertThat( + debugEvent.additionalProperties?.get(LocalAttribute.Key.REPORTING_SAMPLING_RATE.toString()) + ).isEqualTo( + samplingRate + ) + } + } + + @Test + fun `M send metric W metric() {sampling x percent}`( + @StringForgery fakeMessage: String, + @FloatForgery(25f, 75f) fakeSampleRate: Float, + forge: Forge + ) { + // Given + val fakeAdditionalProperties = forge.exhaustiveAttributes() + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + val repeatCount = 100 + val expectedCallCount = (repeatCount * fakeSampleRate / 100f).toInt() + val marginOfError = (repeatCount * 0.25f).toInt() + + // When + repeat(100) { + testedInternalLogger.logMetric( + mockLambda, + fakeAdditionalProperties, + fakeSampleRate + ) + } + + // Then + val count = mockingDetails(mockRumFeatureScope).invocations.filter { it.method.name == "sendEvent" }.size + assertThat(count).isCloseTo(expectedCallCount, offset(marginOfError)) + } + + @Test + fun `M not send metric W metric() {sampling 0 percent}`( + @StringForgery fakeMessage: String, + forge: Forge + ) { + // Given + val fakeAdditionalProperties = forge.exhaustiveAttributes() + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + + // When + testedInternalLogger.logMetric( + mockLambda, + fakeAdditionalProperties, + 0.0f + ) + + // Then + verify(mockRumFeatureScope, never()).sendEvent(any()) + } + + @Test + fun `M do nothing W metric { rum feature not initialized }`( + @StringForgery fakeMessage: String, + @FloatForgery(0f, 100f) fakeSampleRate: Float, + forge: Forge + ) { + // Given + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + val fakeAdditionalProperties = forge.exhaustiveAttributes() + val mockLambda: () -> String = mock() + whenever(mockLambda.invoke()) doReturn fakeMessage + + // When + assertDoesNotThrow { + testedInternalLogger.logMetric( + mockLambda, + fakeAdditionalProperties, + fakeSampleRate + ) + } + } + + @Test + fun `M send api usage telemetry W logApiUsage() { sampling rate 100 percent }`( + @Forgery fakeApiUsageInternalTelemetryEvent: InternalTelemetryEvent.ApiUsage + ) { + // When + testedInternalLogger.logApiUsage(100.0f) { fakeApiUsageInternalTelemetryEvent } + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + val apiUsageEvent = firstValue as InternalTelemetryEvent.ApiUsage + assertThat(apiUsageEvent).isEqualTo(fakeApiUsageInternalTelemetryEvent) + } + } + + @Test + fun `M send api usage telemetry W metric() {sampling x percent}`( + @FloatForgery(25f, 75f) fakeSampleRate: Float, + @Forgery fakeApiUsageInternalTelemetryEvent: InternalTelemetryEvent.ApiUsage + ) { + // Given + val repeatCount = 100 + val expectedCallCount = (repeatCount * fakeSampleRate / 100f).toInt() + val marginOfError = (repeatCount * 0.25f).toInt() + + // When + repeat(100) { + testedInternalLogger.logApiUsage(fakeSampleRate) { fakeApiUsageInternalTelemetryEvent } + } + + // Then + val count = mockingDetails(mockRumFeatureScope).invocations.filter { it.method.name == "sendEvent" }.size + assertThat(count).isCloseTo(expectedCallCount, offset(marginOfError)) + } + + @Test + fun `M not send any api usage telemetry W logApiUsage() {sampling 0 percent}`( + @Forgery fakeApiUsageInternalTelemetryEvent: InternalTelemetryEvent.ApiUsage + ) { + // When + testedInternalLogger.logApiUsage(0.0f) { fakeApiUsageInternalTelemetryEvent } + + // Then + verify(mockRumFeatureScope, never()).sendEvent(any()) + } + + @Test + fun `M do nothing W logApiUsage { rum feature not initialized }`( + @FloatForgery(0f, 100f) fakeSampleRate: Float, + @Forgery fakeApiUsageInternalTelemetryEvent: InternalTelemetryEvent.ApiUsage + ) { + // Given + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + // When + assertDoesNotThrow { + testedInternalLogger.logApiUsage(fakeSampleRate) { + fakeApiUsageInternalTelemetryEvent + } + } + } + + @Test + fun `M create PerformanceMetric W startPerformanceMeasure() {MethodCalled, 100 percent}`( + @StringForgery fakeCaller: String, + @StringForgery fakeOperation: String + ) { + // Given + val startNs = System.nanoTime() + + // When + val result = testedInternalLogger.startPerformanceMeasure( + fakeCaller, + TelemetryMetricType.MethodCalled, + 100f, + fakeOperation + ) + val endNs = System.nanoTime() + + // Then + val methodCalledTelemetry = result as? MethodCalledTelemetry + checkNotNull(methodCalledTelemetry) + assertThat(methodCalledTelemetry.callerClass).isEqualTo(fakeCaller) + assertThat(methodCalledTelemetry.operationName).isEqualTo(fakeOperation) + assertThat(methodCalledTelemetry.internalLogger).isSameAs(testedInternalLogger) + assertThat(methodCalledTelemetry.startTime).isBetween(startNs, endNs) + } + + @Test + fun `M apply sample rate W startPerformanceMeasure() {MethodCalled, sampled}`( + @StringForgery fakeCaller: String, + @StringForgery fakeOperation: String, + @FloatForgery(min = 25f, max = 75f) fakeSampleRate: Float + ) { + // Given + var sampleCount = 0 + val repeatCount = 256 + val expectedSampledCount = (repeatCount * fakeSampleRate).toInt() / 100 + + // When + repeat(repeatCount) { + val result = testedInternalLogger.startPerformanceMeasure( + fakeCaller, + TelemetryMetricType.MethodCalled, + fakeSampleRate, + fakeOperation + ) + if (result != null) { + sampleCount++ + } + } + + // Then + val margin = (repeatCount / 8) // Allow a 12.5% margin of error + assertThat(sampleCount).isCloseTo(expectedSampledCount, offset(margin)) + } + + private fun InternalLogger.Level.toLogLevel(): Int { + return when (this) { + InternalLogger.Level.VERBOSE -> Log.VERBOSE + InternalLogger.Level.DEBUG -> Log.DEBUG + InternalLogger.Level.INFO -> Log.INFO + InternalLogger.Level.WARN -> Log.WARN + InternalLogger.Level.ERROR -> Log.ERROR + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt new file mode 100644 index 0000000000..5e0a9a4b48 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt @@ -0,0 +1,642 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.internal.time.TimeProvider +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.Locale +import kotlin.math.max + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class BatchMetricsDispatcherTest { + + private lateinit var testedBatchMetricsDispatcher: BatchMetricsDispatcher + + lateinit var fakeFeatureName: String + + @Forgery + lateinit var fakeUploadConfiguration: DataUploadConfiguration + + @Mock + lateinit var mockDateTimeProvider: TimeProvider + + private var currentTimeInMillis: Long = System.currentTimeMillis() + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @IntForgery(min = 0, max = 100) + var fakePendingBatches: Int = 0 + + @Forgery + lateinit var fakeFilePersistenceConfig: FilePersistenceConfig + + @BeforeEach + fun `set up`(forge: Forge) { + fakeFeatureName = forge.anElementFrom( + listOf( + Feature.RUM_FEATURE_NAME, + Feature.TRACING_FEATURE_NAME, + Feature.LOGS_FEATURE_NAME, + Feature.SESSION_REPLAY_FEATURE_NAME, + Feature.SESSION_REPLAY_RESOURCES_FEATURE_NAME + ) + ) + whenever(mockDateTimeProvider.getDeviceTimestamp()).doReturn(currentTimeInMillis) + testedBatchMetricsDispatcher = BatchMetricsDispatcher( + fakeFeatureName, + fakeUploadConfiguration, + fakeFilePersistenceConfig, + mockInternalLogger, + mockDateTimeProvider + ) + } + + @Test + fun `M send metric W sendBatchDeletedMetric`(forge: Forge) { + // Given + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile() + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + } + + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M drop the metric W sendBatchDeletedMetric { time difference is negative }`(forge: Forge) { + // Given + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile() + val newFileName = (currentTimeInMillis + forge.aLong(min = 100, max = 1000)).toString() + whenever(fakeFile.name).thenReturn(newFileName) + + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M send metric W sendBatchDeletedMetric { app in background }`(forge: Forge) { + // Given + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile() + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.IN_BACKGROUND_KEY, true) + } + testedBatchMetricsDispatcher.onPaused() + + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchDeletedMetric { app back in foreground }`(forge: Forge) { + // Given + testedBatchMetricsDispatcher.onPaused() + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile() + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.IN_BACKGROUND_KEY, false) + } + // When + testedBatchMetricsDispatcher.onResumed() + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchDeletedMetric { file is in pending folder }`(forge: Forge) { + // Given + testedBatchMetricsDispatcher.onPaused() + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile().apply { + val forgeAPendingDirName = forge.forgeAPendingDirName() + whenever(this.parentFile?.name).thenReturn(forgeAPendingDirName) + } + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, "pending") + } + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchDeletedMetric { file parent dir is unknown }`(forge: Forge) { + // Given + testedBatchMetricsDispatcher.onPaused() + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile().apply { + whenever(this.parentFile?.name).thenReturn(forge.anAlphabeticalString()) + } + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchDeletedMetric { file parent dir is null }`(forge: Forge) { + // Given + testedBatchMetricsDispatcher.onPaused() + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile().apply { + whenever(this.parentFile).thenReturn(null) + } + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M do nothing W sendBatchDeletedMetric { file name is broken }`(forge: Forge) { + // Given + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = mock { + whenever(it.name).thenReturn(forge.anAlphabeticalString()) + } + + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + verify(mockInternalLogger).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.MAINTAINER), + argThat { + this.invoke() == + BatchMetricsDispatcher.WRONG_FILE_NAME_MESSAGE_FORMAT + .format(Locale.ENGLISH, fakeFile.name) + }, + eq(null), + eq(false), + eq(null) + ) + verifyNoMoreInteractions(mockInternalLogger) + } + + @Test + fun `M do nothing W sendBatchDeletedMetric { feature unknown }`(forge: Forge) { + // Given + val fakeUnknownFeature = forge.anAlphabeticalString() + testedBatchMetricsDispatcher = BatchMetricsDispatcher( + fakeUnknownFeature, + fakeUploadConfiguration, + fakeFilePersistenceConfig, + mockInternalLogger, + mockDateTimeProvider + ) + val fakeReason: RemovalReason.Flushed = forge.getForgery() + val fakeFile: File = forge.forgeValidFile() + + // When + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason, fakePendingBatches) + + // Then + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M send metric W sendBatchClosedMetric`( + @Forgery fakeMetadata: BatchClosedMetadata, + forge: Forge + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile() + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, fakeFile.length()) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M drop the metric W sendBatchClosedMetric { time difference is negative }`( + @Forgery fakeMetadata: BatchClosedMetadata, + forge: Forge + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile() + val fakeFileName = fakeMetadata.lastTimeWasUsedInMs + forge.aLong(min = 100, max = 1000) + whenever(fakeFile.name).thenReturn(fakeFileName.toString()) + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M send metric W sendBatchClosedMetric{ file is broken }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata, + @Forgery fakeException: Exception + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) + } + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchClosedMetric{ file is in pending folder }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata, + @Forgery fakeException: Exception + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) + whenever(this.parentFile?.name).thenReturn(forge.forgeAPendingDirName()) + } + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, "pending") + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchClosedMetric{ file parent dir is null }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata, + @Forgery fakeException: Exception + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) + whenever(this.parentFile).thenReturn(null) + } + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchClosedMetric{ file parent dir is unknown }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata, + @Forgery fakeException: Exception + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) + whenever(this.parentFile?.name).thenReturn(forge.anAlphabeticalString()) + } + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture(), + eq(1.5f), + eq(null) + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M do nothing W sendBatchClosedMetric { file name is broken }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.name).thenReturn(forge.anAlphabeticalString()) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + verify(mockInternalLogger).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.MAINTAINER), + argThat { + this.invoke() == + BatchMetricsDispatcher.WRONG_FILE_NAME_MESSAGE_FORMAT + .format(Locale.ENGLISH, fakeFile.name) + }, + eq(null), + eq(false), + eq(null) + ) + verifyNoMoreInteractions(mockInternalLogger) + } + + @Test + fun `M do nothing W sendBatchClosedMetric { file does not exist }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.exists()).thenReturn(false) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M do nothing W sendBatchClosedMetric { feature unknown }`( + @Forgery fakeMetadata: BatchClosedMetadata, + forge: Forge + ) { + // Given + val fakeUnknownFeature = forge.anAlphabeticalString() + testedBatchMetricsDispatcher = BatchMetricsDispatcher( + fakeUnknownFeature, + fakeUploadConfiguration, + fakeFilePersistenceConfig, + mockInternalLogger, + mockDateTimeProvider + ) + val fakeFile: File = forge.forgeValidClosedFile() + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + verifyNoInteractions(mockInternalLogger) + } + + private fun resolveDefaultDeleteExtraProperties(file: File): MutableMap { + return mutableMapOf( + BatchMetricsDispatcher.TYPE_KEY to BatchMetricsDispatcher.BATCH_DELETED_TYPE_VALUE, + BatchMetricsDispatcher.TRACK_KEY to resolveTrackName(fakeFeatureName), + BatchMetricsDispatcher.BATCH_AGE_KEY to max(0, (currentTimeInMillis - file.name.toLong())), + BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to + fakeFilePersistenceConfig.recentDelayMs, + BatchMetricsDispatcher.UPLOADER_DELAY_KEY to mapOf( + BatchMetricsDispatcher.UPLOADER_DELAY_MIN_KEY to + fakeUploadConfiguration.minDelayMs, + BatchMetricsDispatcher.UPLOADER_DELAY_MAX_KEY to + fakeUploadConfiguration.maxDelayMs + ), + BatchMetricsDispatcher.FILE_NAME to file.name, + BatchMetricsDispatcher.THREAD_NAME to Thread.currentThread().name, + BatchMetricsDispatcher.TRACKING_CONSENT_KEY to "granted", + BatchMetricsDispatcher.PENDING_BATCHES to fakePendingBatches, + BatchMetricsDispatcher.IN_BACKGROUND_KEY to true + ) + } + + private fun resolveDefaultCloseExtraProperties( + file: File, + batchClosedMetadata: BatchClosedMetadata + ): MutableMap { + return mutableMapOf( + BatchMetricsDispatcher.TYPE_KEY to BatchMetricsDispatcher.BATCH_CLOSED_TYPE_VALUE, + BatchMetricsDispatcher.TRACK_KEY to resolveTrackName(fakeFeatureName), + BatchMetricsDispatcher.BATCH_DURATION_KEY to + max(0, (batchClosedMetadata.lastTimeWasUsedInMs - file.name.toLong())), + BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to + fakeFilePersistenceConfig.recentDelayMs, + BatchMetricsDispatcher.BATCH_EVENTS_COUNT_KEY to batchClosedMetadata.eventsCount, + BatchMetricsDispatcher.FILE_NAME to file.name, + BatchMetricsDispatcher.THREAD_NAME to Thread.currentThread().name, + BatchMetricsDispatcher.TRACKING_CONSENT_KEY to "granted" + ) + } + + private fun Forge.forgeValidFile(): File { + val fileNameAsLong = currentTimeInMillis - aLong(min = 1000, max = 100000) + val fileLength = aPositiveLong() + val dirName = forgeAGrantedDirName() + val parentDirectory: File = mock { + whenever(it.isDirectory).thenReturn(true) + whenever(it.name).thenReturn(dirName) + } + val fakeFile: File = mock { + whenever(it.parentFile).thenReturn(parentDirectory) + whenever(it.name).thenReturn(fileNameAsLong.toString()) + whenever(it.length()).thenReturn(fileLength) + } + return fakeFile + } + + private fun Forge.forgeValidClosedFile(): File { + return forgeValidFile().apply { whenever(this.exists()).thenReturn(true) } + } + + private fun Forge.forgeAGrantedDirName(): String { + val separator = "-" + return aList(anInt(min = 1, max = 10)) { anAlphabeticalString() } + .joinToString(separator) + "-v" + aNumericalString() + } + + private fun Forge.forgeAPendingDirName(): String { + val separator = "-" + return aList(anInt(min = 1, max = 10)) { anAlphabeticalString() } + .joinToString(separator) + "-pending-v" + aNumericalString() + } + + private fun resolveTrackName(featureName: String): String? { + return when (featureName) { + Feature.RUM_FEATURE_NAME -> BatchMetricsDispatcher.RUM_TRACK_NAME + Feature.LOGS_FEATURE_NAME -> BatchMetricsDispatcher.LOGS_TRACK_NAME + Feature.TRACING_FEATURE_NAME -> BatchMetricsDispatcher.TRACE_TRACK_NAME + Feature.SESSION_REPLAY_FEATURE_NAME -> BatchMetricsDispatcher.SR_TRACK_NAME + Feature.SESSION_REPLAY_RESOURCES_FEATURE_NAME -> BatchMetricsDispatcher.SR_RESOURCES_TRACK_NAME + else -> null + } + } + + private fun Forge.forgeIncludeInMetricReason(): RemovalReason { + while (true) { + val reason: RemovalReason = getForgery() + if (reason.includeInMetrics()) { + return reason + } + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/MethodCalledTelemetryTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/MethodCalledTelemetryTest.kt new file mode 100644 index 0000000000..2485f16e0d --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/MethodCalledTelemetryTest.kt @@ -0,0 +1,190 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.metrics + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.DeviceInfo +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry.Companion.CALLER_CLASS +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry.Companion.EXECUTION_TIME +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry.Companion.IS_SUCCESSFUL +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry.Companion.METHOD_CALLED_METRIC_NAME +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry.Companion.METRIC_TYPE_VALUE +import com.datadog.android.core.internal.metrics.MethodCalledTelemetry.Companion.OPERATION_NAME +import com.datadog.android.core.metrics.PerformanceMetric.Companion.METRIC_TYPE +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class MethodCalledTelemetryTest { + private lateinit var testedMethodCalledTelemetry: MethodCalledTelemetry + + @StringForgery + private lateinit var fakeCallerClass: String + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockInternalSdkCore: InternalSdkCore + + @Mock + lateinit var mockDatadogContext: DatadogContext + + @Mock + lateinit var mockDeviceInfo: DeviceInfo + + @StringForgery + lateinit var fakeDeviceModel: String + + @StringForgery + lateinit var fakeDeviceBrand: String + + @StringForgery + lateinit var fakeOsName: String + + @StringForgery + lateinit var fakeOsVersion: String + + @StringForgery + lateinit var fakeOsBuild: String + + @StringForgery + lateinit var fakeDeviceArchitecture: String + + @StringForgery + lateinit var fakeOperationName: String + + @FloatForgery(min = 0.1f, max = 100f) + private var fakeCreationSampleRate: Float = 0.1f + + private var fakeStartTime: Long = 0 + + private var fakeStatus: Boolean = false + + private val lambdaCaptor = argumentCaptor<() -> String>() + private val mapCaptor = argumentCaptor>() + + @BeforeEach + fun setup(forge: Forge) { + fakeStatus = forge.aBool() + + whenever(mockInternalSdkCore.getDatadogContext()).thenReturn(mockDatadogContext) + whenever(mockDatadogContext.deviceInfo).thenReturn(mockDeviceInfo) + whenever(mockDeviceInfo.deviceModel).thenReturn(fakeDeviceModel) + whenever(mockDeviceInfo.deviceBrand).thenReturn(fakeDeviceBrand) + whenever(mockDeviceInfo.architecture).thenReturn(fakeDeviceArchitecture) + whenever(mockDeviceInfo.osName).thenReturn(fakeOsName) + whenever(mockDeviceInfo.osVersion).thenReturn(fakeOsVersion) + whenever(mockDeviceInfo.deviceBuildId).thenReturn(fakeOsBuild) + + fakeStartTime = System.nanoTime() + testedMethodCalledTelemetry = MethodCalledTelemetry( + internalLogger = mockInternalLogger, + operationName = fakeOperationName, + callerClass = fakeCallerClass, + startTime = fakeStartTime, + creationSampleRate = fakeCreationSampleRate + ) + } + + @Test + fun `M call logger with correct title W sendMetric()`() { + // When + testedMethodCalledTelemetry.stopAndSend(false) + + // Then + verify(mockInternalLogger).logMetric(lambdaCaptor.capture(), any(), eq(100.0f), eq(fakeCreationSampleRate)) + lambdaCaptor.firstValue.run { + val title = this() + assertThat(title).isEqualTo(METHOD_CALLED_METRIC_NAME) + } + } + + @Test + fun `M call logger with correct execution time W sendMetric()`() { + // When + testedMethodCalledTelemetry.stopAndSend(false) + + // Then + verify(mockInternalLogger).logMetric(any(), mapCaptor.capture(), eq(100.0f), eq(fakeCreationSampleRate)) + val executionTime = mapCaptor.firstValue[EXECUTION_TIME] as Long + + assertThat(executionTime).isLessThan(System.nanoTime() - fakeStartTime) + } + + @Test + fun `M call logger with correct operation name W sendMetric()`() { + // When + testedMethodCalledTelemetry.stopAndSend(false) + + // Then + verify(mockInternalLogger).logMetric(any(), mapCaptor.capture(), eq(100.0f), eq(fakeCreationSampleRate)) + val operationName = mapCaptor.firstValue[OPERATION_NAME] as String + + assertThat(operationName).isEqualTo(fakeOperationName) + } + + @Test + fun `M call logger with correct caller class W sendMetric()`() { + // When + testedMethodCalledTelemetry.stopAndSend(false) + + // Then + verify(mockInternalLogger).logMetric(any(), mapCaptor.capture(), eq(100.0f), eq(fakeCreationSampleRate)) + val callerClass = mapCaptor.firstValue[CALLER_CLASS] as String + + assertThat(callerClass).isEqualTo(fakeCallerClass) + } + + @Test + fun `M call logger with correct isSuccessful value W sendMetric()`() { + // When + testedMethodCalledTelemetry.stopAndSend(fakeStatus) + + // Then + verify(mockInternalLogger).logMetric(any(), mapCaptor.capture(), eq(100.0f), eq(fakeCreationSampleRate)) + val isSuccessful = mapCaptor.firstValue[IS_SUCCESSFUL] as Boolean + + assertThat(isSuccessful).isEqualTo(fakeStatus) + } + + @Test + fun `M call logger with correct metric type value W sendMetric()`() { + // When + testedMethodCalledTelemetry.stopAndSend(fakeStatus) + + // Then + verify(mockInternalLogger).logMetric(any(), mapCaptor.capture(), eq(100.0f), eq(fakeCreationSampleRate)) + val metricTypeValue = mapCaptor.firstValue[METRIC_TYPE] as String + + assertThat(metricTypeValue).isEqualTo(METRIC_TYPE_VALUE) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolverTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolverTest.kt new file mode 100644 index 0000000000..57c8fae0b2 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolverTest.kt @@ -0,0 +1,372 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.net + +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DefaultFirstPartyHostHeaderTypeResolverTest { + + lateinit var testedDetector: DefaultFirstPartyHostHeaderTypeResolver + + lateinit var fakeHosts: Map> + + @BeforeEach + fun `set up`(forge: Forge) { + fakeHosts = forge.aMap { + forge.aStringMatching(HOST_REGEX) to + forge.aList { aValueFrom(TracingHeaderType::class.java) }.toSet() + } + testedDetector = DefaultFirstPartyHostHeaderTypeResolver(fakeHosts) + } + + @Test + fun `M return false W isFirstParty(HttpUrl) {unknown host}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + var host = forge.aStringMatching(HOST_REGEX) + while (host in fakeHosts) { + host = forge.aStringMatching(HOST_REGEX) + } + val url = "$scheme://$host$path".toHttpUrl() + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return true W isFirstParty(HttpUrl) {exact first party host}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val host = forge.anElementFrom(fakeHosts.keys) + val url = "$scheme://$host$path".toHttpUrl() + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return true W isFirstParty(HttpUrl) {known hosts list was updated}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val fakeNewAllowedHosts = forge.aList { forge.aStringMatching(HOST_REGEX) } + testedDetector.addKnownHosts(fakeNewAllowedHosts) + val host = forge.anElementFrom(fakeNewAllowedHosts) + val url = "$scheme://$host$path".toHttpUrl() + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return true W isFirstParty(HttpUrl) {valid host subdomain}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") subdomain: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val host = forge.anElementFrom(fakeHosts.keys) + val url = "$scheme://$subdomain.$host$path".toHttpUrl() + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W isFirstParty(HttpUrl) {unknown host postfixed with valid host}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") prefix: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val host = forge.anElementFrom(fakeHosts.keys) + val url = "$scheme://$prefix$host$path".toHttpUrl() + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return false W isFirstParty(String) {unknown host}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + var host = forge.aStringMatching(HOST_REGEX) + while (host in fakeHosts.keys) { + host = forge.aStringMatching(HOST_REGEX) + } + val url = "$scheme://$host$path" + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return true W isFirstParty(String) {exact first party host}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val host = forge.anElementFrom(fakeHosts.keys) + val url = "$scheme://$host$path" + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return true W isFirstParty(String) {valid host subdomain}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") subdomain: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val host = forge.anElementFrom(fakeHosts.keys) + val url = "$scheme://$subdomain.$host$path" + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return true W isFirstParty(String) {known hosts list was updated}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val fakeNewAllowedHosts = forge.aList { forge.aStringMatching(HOST_REGEX) } + testedDetector.addKnownHosts(fakeNewAllowedHosts) + val host = forge.anElementFrom(fakeNewAllowedHosts) + val url = "$scheme://$host$path" + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return true isFirstParty(String) { wild card used for known hosts }`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // GIVEN + val fakeNewAllowedHosts = forge.aList { forge.aStringMatching(HOST_REGEX) } + val host = forge.anElementFrom(fakeNewAllowedHosts) + val url = "$scheme://$host$path" + testedDetector = DefaultFirstPartyHostHeaderTypeResolver(mapOf("*" to emptySet())) + + // WHEN + val result = testedDetector.isFirstPartyUrl(url) + + // THEN + assertThat(result).isTrue() + } + + @Test + fun `M return true isFirstParty(HttpUrl) { wild card used for known hosts }`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // GIVEN + val fakeNewAllowedHosts = forge.aList { forge.aStringMatching(HOST_REGEX) } + val host = forge.anElementFrom(fakeNewAllowedHosts) + val url = "$scheme://$host$path".toHttpUrl() + testedDetector = DefaultFirstPartyHostHeaderTypeResolver(mapOf("*" to emptySet())) + + // WHEN + val result = testedDetector.isFirstPartyUrl(url) + + // THEN + assertThat(result).isTrue() + } + + @Test + fun `M return false W isFirstParty(String) {unknown host postfixed with valid host}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") prefix: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, + forge: Forge + ) { + // Given + val host = forge.anElementFrom(fakeHosts.keys) + val url = "$scheme://$prefix$host$path" + + // When + val result = testedDetector.isFirstPartyUrl(url) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return false W isFirstParty(String) {invalid url}`( + @StringForgery notAUrl: String + ) { + // When + val result = testedDetector.isFirstPartyUrl(notAUrl) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return true W isEmpty() {empty host list}`() { + // Given + val resolver = DefaultFirstPartyHostHeaderTypeResolver(emptyMap()) + + // When + val result = resolver.isEmpty() + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W isEmpty() {non empty host list}`() { + // When + val result = testedDetector.isEmpty() + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return header types W headerTypesForUrl(String) {first party hosts}`( + @StringForgery(regex = "http(s?)") scheme: String, + @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String + ) { + for ((host, tracingHeaders) in fakeHosts) { + // Given + val url = "$scheme://$host$path" + + // When + val detectedHeader = testedDetector.headerTypesForUrl(url) + + // Then + assertThat(detectedHeader).isEqualTo(tracingHeaders) + } + } + + @Test + fun `M return all header types W getAllHeaderTypes() {first party hosts}`() { + // Given + var allUsedHeaderTraces = mutableSetOf() + + // When + for ((_, tracingHeaders) in fakeHosts) { + allUsedHeaderTraces = + allUsedHeaderTraces.plus(tracingHeaders) as MutableSet + } + + // Then + assertThat(testedDetector.getAllHeaderTypes()).isEqualTo(allUsedHeaderTraces) + } + + @Test + fun `M use datadog and tracecontext header types W addKnownHosts(String)`( + @StringForgery(regex = "http(s?)") scheme: String, + forge: Forge + ) { + // Given + val fakeHosts = forge.aList { forge.aStringMatching(HOST_REGEX) } + val fakeUrls = fakeHosts.map { "$scheme://$it" } + testedDetector.addKnownHosts(fakeHosts) + + // When + Then + val expectedHeaderTypes = setOf( + TracingHeaderType.DATADOG, + TracingHeaderType.TRACECONTEXT + ) + fakeUrls.forEach { + val headerTypes = testedDetector.headerTypesForUrl(it) + assertThat(headerTypes).isEqualTo(expectedHeaderTypes) + } + } + + @Test + fun `M return correct header type W headerTypesForUrl(String) {domain and subdomain has different types}`() { + val resolver = DefaultFirstPartyHostHeaderTypeResolver( + mapOf( + "bar.com" to setOf(TracingHeaderType.DATADOG), + "foo.bar.com" to setOf(TracingHeaderType.TRACECONTEXT) + ) + ) + + assertThat(resolver.headerTypesForUrl("http://bar.com/")) + .isEqualTo(setOf(TracingHeaderType.DATADOG)) + + assertThat(resolver.headerTypesForUrl("http://foo.bar.com/")) + .isEqualTo(setOf(TracingHeaderType.TRACECONTEXT)) + } + + companion object { + private const val HOST_REGEX = "([a-z][a-z0-9_~-]{3,9}\\.){1,4}[a-z][a-z0-9]{2,3}" + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt new file mode 100644 index 0000000000..a3bcf57e0a --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt @@ -0,0 +1,611 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +@file:Suppress("DEPRECATION") + +package com.datadog.android.core.internal.net.info + +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.os.Build +import android.telephony.TelephonyManager +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.core.internal.system.BuildSdkVersionProvider +import com.datadog.android.utils.assertj.NetworkInfoAssert.Companion.assertThat +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.stream.Stream +import android.net.NetworkInfo as AndroidNetworkInfo + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +@Suppress("DEPRECATION") +internal class BroadcastReceiverNetworkInfoProviderTest { + + lateinit var testedProvider: BroadcastReceiverNetworkInfoProvider + + @Mock + lateinit var mockContext: Context + + @Mock + lateinit var mockConnectivityManager: ConnectivityManager + + @Mock + lateinit var mockTelephonyManager: TelephonyManager + + @Mock + lateinit var mockNetworkInfo: AndroidNetworkInfo + + @Mock + lateinit var mockIntent: Intent + + @Mock + lateinit var mockBuildSdkVersionProvider: BuildSdkVersionProvider + + @BeforeEach + fun `set up`() { + whenever(mockContext.getSystemService(Context.CONNECTIVITY_SERVICE)) + .doReturn(mockConnectivityManager) + whenever(mockContext.getSystemService(Context.TELEPHONY_SERVICE)) + .doReturn(mockTelephonyManager) + whenever(mockConnectivityManager.activeNetworkInfo) doReturn mockNetworkInfo + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.BASE + + testedProvider = BroadcastReceiverNetworkInfoProvider(mockBuildSdkVersionProvider) + } + + @Test + fun `initial state is not connected`() { + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @Test + fun `it will do nothing if unregister is called before register`() { + // When + testedProvider.unregister(mockContext) + + // Then + verifyNoInteractions(mockContext) + } + + @Test + fun `it will unregister the receiver only once`() { + // Given + val countDownLatch = CountDownLatch(2) + testedProvider.register(mockContext) + + // When + Thread { + testedProvider.unregister(mockContext) + countDownLatch.countDown() + }.start() + Thread { + testedProvider.unregister(mockContext) + countDownLatch.countDown() + }.start() + + // Then + countDownLatch.await(3, TimeUnit.SECONDS) + verify(mockContext).unregisterReceiver(testedProvider) + } + + @Test + fun `read network info on register`() { + stubNetworkInfo(ConnectivityManager.TYPE_WIFI, -1) + + testedProvider.register(mockContext) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @Test + fun `M delegate to persister W onReceive`() { + stubNetworkInfo(ConnectivityManager.TYPE_WIFI, -1) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @Test + fun `not connected (null)`() { + whenever(mockConnectivityManager.activeNetworkInfo) doReturn null + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @Test + fun `not connected (connected but no internet)`() { + // @hide ConnectivityManager.TYPE_NONE = -1 + stubNetworkInfo(-1, -1) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @Test + fun `connected to wifi`() { + stubNetworkInfo(ConnectivityManager.TYPE_WIFI, -1) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @Test + fun `connected to ethernet`() { + stubNetworkInfo(ConnectivityManager.TYPE_ETHERNET, -1) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_ETHERNET) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @ParameterizedTest + @MethodSource("2gSubtypeToMobileTypes") + fun `connected to mobile 2G`(subtype: NetworkType, mobileType: MobileType, forge: Forge) { + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_2G) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("2gSubtypeToMobileTypes") + fun `connected to mobile 2G API 28+`( + subtype: NetworkType, + mobileType: MobileType, + forge: Forge + ) { + // GIVEN + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P + + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + // WHEN + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_2G) + .hasCarrierName(carrierName) + .hasCarrierId(carrierId.toLong()) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("3gSubtypeToMobileTypes") + fun `connected to mobile 3G`(subtype: NetworkType, mobileType: MobileType, forge: Forge) { + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_3G) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("3gSubtypeToMobileTypes") + fun `connected to mobile 3G API 28+`( + subtype: NetworkType, + mobileType: MobileType, + forge: Forge + ) { + // GIVEN + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P + + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + // WHEN + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_3G) + .hasCarrierName(carrierName) + .hasCarrierId(carrierId.toLong()) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("4gSubtypeToMobileTypes") + fun `connected to mobile 4G`(subtype: NetworkType, mobileType: MobileType, forge: Forge) { + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_4G) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("4gSubtypeToMobileTypes") + fun `connected to mobile 4G API 28+`( + subtype: NetworkType, + mobileType: MobileType, + forge: Forge + ) { + // GIVEN + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P + + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + // WHEN + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_4G) + .hasCarrierName(carrierName) + .hasCarrierId(carrierId.toLong()) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("5gSubtypeToMobileTypes") + fun `connected to mobile 5G`(subtype: NetworkType, mobileType: MobileType, forge: Forge) { + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_5G) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("5gSubtypeToMobileTypes") + fun `connected to mobile 5G API 28+`( + subtype: NetworkType, + mobileType: MobileType, + forge: Forge + ) { + // GIVEN + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P + + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype.id) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + // WHEN + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_5G) + .hasCarrierName(carrierName) + .hasCarrierId(carrierId.toLong()) + .hasCellularTechnology(mobileSubtypeNames[subtype.id]) + } + + @ParameterizedTest + @MethodSource("getKnownMobileTypes") + fun `connected to mobile unknown`(mobileType: MobileType, forge: Forge) { + val subtype = forge.anInt(min = 32) + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + @ParameterizedTest + @MethodSource("getKnownMobileTypes") + fun `connected to mobile unknown API 28+`(mobileType: MobileType, forge: Forge) { + // GIVEN + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P + + val subtype = forge.anInt(min = 32) + val carrierName = forge.anAlphabeticalString() + val carrierId = forge.aPositiveInt(strict = true) + stubNetworkInfo(mobileType.id, subtype) + stubTelephonyManager(carrierName, carrierId) + testedProvider.onReceive(mockContext, mockIntent) + + // WHEN + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER) + .hasCarrierName(carrierName) + .hasCarrierId(carrierId.toLong()) + .hasCellularTechnology(null) + } + + @ParameterizedTest + @MethodSource("getKnownMobileTypes") + fun `connected to mobile unknown carrier`(mobileType: MobileType) { + // GIVEN + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P + + stubNetworkInfo( + mobileType.id, + TelephonyManager.NETWORK_TYPE_UNKNOWN + ) + stubTelephonyManager(null, 0) + testedProvider.onReceive(mockContext, mockIntent) + + // WHEN + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER) + .hasCarrierName("Unknown Carrier Name") + .hasCarrierId(0) + .hasCellularTechnology(null) + } + + @Test + fun `connected to unknown network`(forge: Forge) { + stubNetworkInfo(forge.anInt(min = 6), -1) + testedProvider.onReceive(mockContext, mockIntent) + + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasCellularTechnology(null) + } + + // region Internal + + private fun stubTelephonyManager(carrierName: String?, carrierId: Int) { + whenever(mockTelephonyManager.simCarrierIdName) doReturn carrierName + whenever(mockTelephonyManager.simCarrierId) doReturn carrierId + } + + private fun stubNetworkInfo(networkType: Int, networkSubtype: Int) { + if (networkType < 0) { + whenever(mockNetworkInfo.isConnected) doReturn false + whenever(mockNetworkInfo.type) doReturn -1 + } else { + whenever(mockNetworkInfo.isConnected) doReturn true + whenever(mockNetworkInfo.type) doReturn networkType + } + whenever(mockNetworkInfo.subtype) doReturn networkSubtype + } + + // endregion + + // ConnectivityManager.TYPE_MOBILE_XXX + data class MobileType(val name: String, val id: Int) + + // TelephonyManager.NETWORK_TYPE_XXX + data class NetworkType(val name: String, val id: Int) + + companion object { + + private val mobileTypeNames = mapOf( + ConnectivityManager.TYPE_MOBILE to "Mobile", + ConnectivityManager.TYPE_MOBILE_DUN to "Mobile_DUN", + ConnectivityManager.TYPE_MOBILE_HIPRI to "Mobile_HIPRI", + ConnectivityManager.TYPE_MOBILE_MMS to "Mobile_MSS", + ConnectivityManager.TYPE_MOBILE_SUPL to "Mobile_SUPL" + ) + + private val mobileSubtypeNames = arrayOf( + "unknown", "GPRS", "Edge", "UMTS", "CDMA", "CDMAEVDORev0", "CDMAEVDORevA", "CDMA1x", + "HSDPA", "HSUPA", "HSPA", "iDen", "CDMAEVDORevB", "LTE", "eHRPD", "HSPA+", "GSM", + "TD_SCDMA", "IWLAN", "LTE_CA", "New Radio" + ) + + internal val knownMobileTypes = listOf( + ConnectivityManager.TYPE_MOBILE, + ConnectivityManager.TYPE_MOBILE_DUN, + ConnectivityManager.TYPE_MOBILE_HIPRI, + ConnectivityManager.TYPE_MOBILE_MMS, + ConnectivityManager.TYPE_MOBILE_SUPL + ) + + internal val known2GSubtypes = listOf( + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT, + TelephonyManager.NETWORK_TYPE_IDEN, + TelephonyManager.NETWORK_TYPE_GSM + ) + + internal val known3GSubtypes = listOf( + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_HSUPA, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_EVDO_B, + TelephonyManager.NETWORK_TYPE_EHRPD, + TelephonyManager.NETWORK_TYPE_HSPAP, + TelephonyManager.NETWORK_TYPE_TD_SCDMA + ) + internal val known4GSubtypes = listOf( + TelephonyManager.NETWORK_TYPE_LTE, + TelephonyManager.NETWORK_TYPE_IWLAN, + 19 // @Hide TelephonyManager.NETWORK_TYPE_LTE_CA, + ) + internal val known5GSubtypes = listOf( + TelephonyManager.NETWORK_TYPE_NR + ) + + private val knownMobileTypesWithNames = knownMobileTypes + .map { MobileType(mobileTypeNames.getValue(it), it) } + + private val known2GSubtypesWithNames = + known2GSubtypes.map { NetworkType(mobileSubtypeNames[it], it) } + private val known3GSubtypesWithNames = + known3GSubtypes.map { NetworkType(mobileSubtypeNames[it], it) } + private val known4GSubtypesWithNames = + known4GSubtypes.map { NetworkType(mobileSubtypeNames[it], it) } + private val known5GSubtypesWithNames = + known5GSubtypes.map { NetworkType(mobileSubtypeNames[it], it) } + + @Suppress("unused") + @JvmStatic + fun `2gSubtypeToMobileTypes`(): Stream { + return allCombinations(known2GSubtypesWithNames, knownMobileTypesWithNames) + .map { Arguments.of(it.first, it.second) } + .stream() + } + + @Suppress("unused") + @JvmStatic + fun `3gSubtypeToMobileTypes`(): Stream { + return allCombinations(known3GSubtypesWithNames, knownMobileTypesWithNames) + .map { Arguments.of(it.first, it.second) } + .stream() + } + + @Suppress("unused") + @JvmStatic + fun `4gSubtypeToMobileTypes`(): Stream { + return allCombinations(known4GSubtypesWithNames, knownMobileTypesWithNames) + .map { Arguments.of(it.first, it.second) } + .stream() + } + + @Suppress("unused") + @JvmStatic + fun `5gSubtypeToMobileTypes`(): Stream { + return allCombinations(known5GSubtypesWithNames, knownMobileTypesWithNames) + .map { Arguments.of(it.first, it.second) } + .stream() + } + + @Suppress("unused") + @JvmStatic + fun getKnownMobileTypes(): List { + return knownMobileTypesWithNames + } + + private fun allCombinations( + networkTypes: Iterable, + mobileTypes: Iterable + ): Iterable> { + return networkTypes + .flatMap { item -> + mobileTypes.map { item to it } + } + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt new file mode 100644 index 0000000000..54b9da7e6b --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt @@ -0,0 +1,581 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.net.info + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Build +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.core.internal.system.BuildSdkVersionProvider +import com.datadog.android.utils.assertj.NetworkInfoAssert.Companion.assertThat +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class CallbackNetworkInfoProviderTest { + + lateinit var testedProvider: CallbackNetworkInfoProvider + + @Mock + lateinit var mockNetwork: Network + + @Mock + lateinit var mockCapabilities: NetworkCapabilities + + @Mock + lateinit var mockBuildSdkVersionProvider: BuildSdkVersionProvider + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + // setup the network capabilities to return the unspecified values by default + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn 0 + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn 0 + whenever(mockCapabilities.signalStrength) doReturn + NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED + whenever(mockCapabilities.hasTransport(any())) doReturn false + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.BASE + + testedProvider = CallbackNetworkInfoProvider(mockBuildSdkVersionProvider, mockInternalLogger) + } + + @Test + fun `initial state is not connected`() { + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(null) + .hasDownSpeed(null) + .hasStrength(null) + } + + @Test + fun `connected to wifi`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int, + @IntForgery(min = -90, max = -40) strength: Int + ) { + // GIVEN + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) doReturn true + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + whenever(mockCapabilities.signalStrength) doReturn strength + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.Q + + // WHEN + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + .hasStrength(strength.toLong()) + } + + @Test + fun `connected to wifi (no strength)`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + // GIVEN + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) doReturn true + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + // WHEN + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + .hasStrength(null) + } + + @Test + fun `connected to wifi (no up or down bandwidth, no strength)`() { + // GIVEN + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) doReturn true + + // WHEN + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(null) + .hasDownSpeed(null) + .hasStrength(null) + } + + @Test + fun `connected to wifi aware`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int, + @IntForgery(min = -90, max = -40) strength: Int + ) { + // GIVEN + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) + .doReturn(true) + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + whenever(mockCapabilities.signalStrength) doReturn strength + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.Q + + // WHEN + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + .hasStrength(strength.toLong()) + } + + @Test + fun `connected to wifi aware (no strength)`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + // GIVEN + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) + .doReturn(true) + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + // WHEN + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + .hasStrength(null) + } + + @Test + fun `connected to wifi aware (no up or down bandwidth, no strength)`() { + // GIVEN + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) + .doReturn(true) + + // WHEN + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + // THEN + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(null) + .hasDownSpeed(null) + .hasStrength(null) + } + + @Test + fun `connected to cellular`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) + .doReturn(true) + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_CELLULAR) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + } + + @Test + fun `connected to ethernet`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) + .doReturn(true) + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_ETHERNET) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + } + + @Test + fun `connected to VPN`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) doReturn true + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + .hasStrength(null) + } + + @Test + fun `connected to LoWPAN`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN)) + .doReturn(true) + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + .hasStrength(null) + } + + @Test + fun `network lost`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + whenever(mockCapabilities.hasTransport(any())) doReturn true + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) + testedProvider.onLost(mockNetwork) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(null) + .hasDownSpeed(null) + .hasStrength(null) + } + + @Test + fun `M register callback W register()`() { + val context = mock() + val manager = mock() + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + + testedProvider.register(context) + + verify(manager).registerDefaultNetworkCallback(testedProvider) + } + + @Test + fun `M get current network state W register()`( + @IntForgery(min = 1) upSpeed: Int, + @IntForgery(min = 1) downSpeed: Int + ) { + val context = mock() + val manager = mock() + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.activeNetwork) doReturn mockNetwork + whenever(manager.getNetworkCapabilities(mockNetwork)) doReturn mockCapabilities + whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) doReturn true + whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed + whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed + + testedProvider.register(context) + val networkInfo = testedProvider.getLatestNetworkInfo() + + verify(manager).registerDefaultNetworkCallback(testedProvider) + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(upSpeed.toLong()) + .hasDownSpeed(downSpeed.toLong()) + .hasStrength(null) + } + + @Test + fun `M register callback safely W register() with SecurityException`( + @StringForgery message: String + ) { + // RUMM-852 in some cases the device throws a SecurityException on register + val context = mock() + val manager = mock() + val exception = SecurityException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception + + testedProvider.register(context) + + verify(manager).registerDefaultNetworkCallback(testedProvider) + } + + @Test + fun `M warn developers W register() with null service`() { + val context = mock() + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn null + + testedProvider.register(context) + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + CallbackNetworkInfoProvider.ERROR_REGISTER + ) + } + + @Test + fun `M warn developers W register() with SecurityException`( + @StringForgery message: String + ) { + // RUMM-852 in some cases the device throws a SecurityException on register + val context = mock() + val manager = mock() + val exception = SecurityException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception + + testedProvider.register(context) + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + CallbackNetworkInfoProvider.ERROR_REGISTER, + exception + ) + } + + @Test + fun `M warn developers W register() with RuntimeException`( + @StringForgery message: String + ) { + // RUMM-918 in some cases the device throws a IllegalArgumentException on register + // "Too many NetworkRequests filed" This happens when registerDefaultNetworkCallback is + // called too many times without matching unregisterNetworkCallback + val context = mock() + val manager = mock() + val exception = RuntimeException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception + + testedProvider.register(context) + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + CallbackNetworkInfoProvider.ERROR_REGISTER, + exception + ) + } + + @Test + fun `M assume network is available W register() with SecurityException + getLatestNetworkInfo`( + @StringForgery message: String + ) { + // RUMM-852 in some cases the device throws a SecurityException on register + val context = mock() + val manager = mock() + val exception = SecurityException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception + + testedProvider.register(context) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(null) + .hasDownSpeed(null) + .hasStrength(null) + } + + @Test + fun `M assume network is available W register() with RuntimeException + getLatestNetworkInfo`( + @StringForgery message: String + ) { + // RUMM-918 in some cases the device throws a IllegalArgumentException on register + // "Too many NetworkRequests filed" This happens when registerDefaultNetworkCallback is + // called too many times without matching unregisterNetworkCallback + val context = mock() + val manager = mock() + val exception = RuntimeException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception + + testedProvider.register(context) + val networkInfo = testedProvider.getLatestNetworkInfo() + + assertThat(networkInfo) + .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) + .hasCarrierName(null) + .hasCarrierId(null) + .hasUpSpeed(null) + .hasDownSpeed(null) + .hasStrength(null) + } + + @Test + fun `M unregister callback W unregister()`() { + val context = mock() + val manager = mock() + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + + testedProvider.unregister(context) + + verify(manager).unregisterNetworkCallback(testedProvider) + } + + @Test + fun `M unregister callback safely W unregister() with SecurityException`( + @StringForgery message: String + ) { + // RUMM-852 in some cases the device throws a SecurityException on register + // Since we can't reproduce, let's assume it could happen on unregister too + val context = mock() + val manager = mock() + val exception = SecurityException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.unregisterNetworkCallback(testedProvider)) doThrow exception + + testedProvider.unregister(context) + + verify(manager).unregisterNetworkCallback(testedProvider) + } + + @Test + fun `M warn developers W unregister() with SecurityException`( + @StringForgery message: String + ) { + // RUMM-852 in some cases the device throws a SecurityException on register + val context = mock() + val manager = mock() + val exception = SecurityException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.unregisterNetworkCallback(testedProvider)) doThrow exception + + testedProvider.unregister(context) + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + CallbackNetworkInfoProvider.ERROR_UNREGISTER, + exception + ) + } + + @Test + fun `M warn developers W unregister() with null service`() { + val context = mock() + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn null + + testedProvider.unregister(context) + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + CallbackNetworkInfoProvider.ERROR_UNREGISTER + ) + } + + @Test + fun `M warn developers W unregister() with RuntimeException`( + @StringForgery message: String + ) { + // RUMM-918 in some cases the device throws a IllegalArgumentException on unregister + // e.g. when the callback was not registered + val context = mock() + val manager = mock() + val exception = RuntimeException(message) + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager + whenever(manager.unregisterNetworkCallback(testedProvider)) doThrow exception + + testedProvider.unregister(context) + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + CallbackNetworkInfoProvider.ERROR_UNREGISTER, + exception + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/AbstractStorageTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/AbstractStorageTest.kt new file mode 100644 index 0000000000..0dbd9a724a --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/AbstractStorageTest.kt @@ -0,0 +1,615 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class AbstractStorageTest { + + private lateinit var testedStorage: AbstractStorage + + @Mock + lateinit var mockPersistenceStrategyFactory: PersistenceStrategy.Factory + + @Mock + lateinit var mockGrantedPersistenceStrategy: PersistenceStrategy + + @Mock + lateinit var mockPendingPersistenceStrategy: PersistenceStrategy + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockConsentProvider: ConsentProvider + + @StringForgery + lateinit var fakeSdkInstanceId: String + + @StringForgery + lateinit var fakeFeatureName: String + + @Forgery + lateinit var fakeStorageConfiguration: FeatureStorageConfiguration + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @Forgery + lateinit var fakeEventType: EventType + + @BeforeEach + fun `set up`() { + whenever(mockPersistenceStrategyFactory.create(argThat { contains("/GRANTED") }, any(), any())) + .doReturn(mockGrantedPersistenceStrategy) + whenever(mockPersistenceStrategyFactory.create(argThat { contains("/PENDING") }, any(), any())) + .doReturn(mockPendingPersistenceStrategy) + + testedStorage = AbstractStorage( + fakeSdkInstanceId, + fakeFeatureName, + mockPersistenceStrategyFactory, + FakeSameThreadExecutorService(), + mockInternalLogger, + fakeStorageConfiguration, + mockConsentProvider + ) + } + + // region Storage.getEventWriteScope + + @Test + fun `M provide writer W getEventWriteScope()+invoke() {consent=granted, batchMetadata=null}`( + @BoolForgery fakeResult: Boolean, + @Forgery fakeBatchEvent: RawBatchEvent + ) { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.GRANTED) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + whenever(mockGrantedPersistenceStrategy.write(any(), anyOrNull(), any())) doReturn fakeResult + var result: Boolean? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + result = it.getArgument(0) + .write(fakeBatchEvent, null, fakeEventType) + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(result).isEqualTo(fakeResult) + verify(mockGrantedPersistenceStrategy).write(fakeBatchEvent, null, fakeEventType) + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide writer W getEventWriteScope()+invoke() {consent=granted, batchMetadata!=null}`( + @BoolForgery fakeResult: Boolean, + @Forgery fakeBatchEvent: RawBatchEvent, + @StringForgery fakeBatchMetadata: String + ) { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.GRANTED) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + val batchMetadata = fakeBatchMetadata.toByteArray() + whenever(mockGrantedPersistenceStrategy.write(any(), anyOrNull(), any())) doReturn fakeResult + var result: Boolean? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + result = it.getArgument(0) + .write(fakeBatchEvent, batchMetadata, fakeEventType) + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(result).isEqualTo(fakeResult) + verify(mockGrantedPersistenceStrategy).write(fakeBatchEvent, batchMetadata, fakeEventType) + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide writer W getEventWriteScope()+currentMetadata() {consent=granted, batchMetadata=null}`() { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.GRANTED) + whenever(mockGrantedPersistenceStrategy.currentMetadata()) doReturn null + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + var resultMetadata: ByteArray? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + resultMetadata = it.getArgument(0).currentMetadata() + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(resultMetadata).isNull() + verify(mockGrantedPersistenceStrategy).currentMetadata() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide writer W getEventWriteScope()+currentMetadata() {consent=granted, batchMetadata!=null}`( + @StringForgery fakeBatchMetadata: String + ) { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.GRANTED) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + val batchMetadata = fakeBatchMetadata.toByteArray() + whenever(mockGrantedPersistenceStrategy.currentMetadata()) doReturn batchMetadata + var resultMetadata: ByteArray? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + resultMetadata = it.getArgument(0).currentMetadata() + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(resultMetadata).isEqualTo(batchMetadata) + verify(mockGrantedPersistenceStrategy).currentMetadata() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide writer W getEventWriteScope()+write() {consent=pending, batchMetadata=null}`( + @BoolForgery fakeResult: Boolean, + @Forgery fakeBatchEvent: RawBatchEvent + ) { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.PENDING) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + whenever(mockPendingPersistenceStrategy.write(any(), anyOrNull(), any())) doReturn fakeResult + var result: Boolean? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + result = it.getArgument(0).write(fakeBatchEvent, null, fakeEventType) + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(result).isEqualTo(fakeResult) + verify(mockPendingPersistenceStrategy).write(fakeBatchEvent, null, fakeEventType) + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide writer W getEventWriteScope()+write() {consent=pending, batchMetadata!=null}`( + @BoolForgery fakeResult: Boolean, + @Forgery fakeBatchEvent: RawBatchEvent, + @StringForgery fakeBatchMetadata: String + ) { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.PENDING) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + val batchMetadata = fakeBatchMetadata.toByteArray() + whenever(mockPendingPersistenceStrategy.write(any(), anyOrNull(), any())) doReturn fakeResult + var result: Boolean? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + result = it.getArgument(0).write(fakeBatchEvent, batchMetadata, fakeEventType) + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(result).isEqualTo(fakeResult) + verify(mockPendingPersistenceStrategy).write(fakeBatchEvent, batchMetadata, fakeEventType) + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide writer W getEventWriteScope()+currentMetadata() {consent=pending, batchMetadata=null}`() { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.PENDING) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + var resultMetadata: ByteArray? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + resultMetadata = it.getArgument(0).currentMetadata() + } + whenever(mockPendingPersistenceStrategy.currentMetadata()) doReturn null + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(resultMetadata).isNull() + verify(mockPendingPersistenceStrategy).currentMetadata() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide writer W getEventWriteScope()+currentMetadata() {consent=pending, batchMetadata!=null}`( + @StringForgery fakeBatchMetadata: String + ) { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.PENDING) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + val batchMetadata = fakeBatchMetadata.toByteArray() + var resultMetadata: ByteArray? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + resultMetadata = it.getArgument(0).currentMetadata() + } + whenever(mockPendingPersistenceStrategy.currentMetadata()) doReturn batchMetadata + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(resultMetadata).isEqualTo(batchMetadata) + verify(mockPendingPersistenceStrategy).currentMetadata() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide no-op writer W getEventWriteScope()+write() {consent=not_granted}`( + @Forgery fakeBatchEvent: RawBatchEvent, + @StringForgery fakeBatchMetadata: String + ) { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.NOT_GRANTED) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + val batchMetadata = fakeBatchMetadata.toByteArray() + var result: Boolean? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + result = it.getArgument(0).write(fakeBatchEvent, batchMetadata, fakeEventType) + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(result).isFalse() + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide no-op writer W getEventWriteScope()+currentMetadata() {consent=not_granted}`() { + // Given + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.NOT_GRANTED) + val mockWriteCallback = mock<(EventBatchWriter) -> Unit>() + var resultMetadata: ByteArray? = null + whenever(mockWriteCallback.invoke(any())) doAnswer { + resultMetadata = it.getArgument(0).currentMetadata() + } + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockWriteCallback) + + // Then + assertThat(resultMetadata).isNull() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + // endregion + + // region Storage.readNextBatch + + @Test + fun `M provide null W readNextBatch() {no batch}`() { + // Given + whenever(mockGrantedPersistenceStrategy.lockAndReadNext()) doReturn null + + // Then + assertThat(testedStorage.readNextBatch()).isNull() + verify(mockGrantedPersistenceStrategy).lockAndReadNext() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M provide BatchData W readNextBatch() {with batch}`( + @Forgery fakeBatch: PersistenceStrategy.Batch + ) { + // Given + whenever(mockGrantedPersistenceStrategy.lockAndReadNext()) doReturn fakeBatch + + // When + val batchData = testedStorage.readNextBatch() + + // Then + assertThat(batchData).isNotNull + assertThat(batchData?.id).isEqualTo(BatchId(fakeBatch.batchId)) + assertThat(batchData?.data).isEqualTo(fakeBatch.events) + assertThat(batchData?.metadata).isEqualTo(fakeBatch.metadata) + } + + @Test + fun `M return null W readNextBatch() {no batch}`() { + // Given + whenever(mockGrantedPersistenceStrategy.lockAndReadNext()) doReturn null + + // Then + assertThat(testedStorage.readNextBatch()).isNull() + } + + // endregion + + // region Storage.readNextBatch + + @Test + fun `M delete batch W confirmBatchRead() {delete=true}`( + @StringForgery fakeBatchId: String, + @Forgery fakeRemovalReason: RemovalReason + ) { + // When + testedStorage.confirmBatchRead(BatchId(fakeBatchId), fakeRemovalReason, true) + + // Then + verify(mockGrantedPersistenceStrategy).unlockAndDelete(fakeBatchId) + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M keep batch W confirmBatchRead() {delete=false}`( + @StringForgery fakeBatchId: String, + @Forgery fakeRemovalReason: RemovalReason + ) { + // When + testedStorage.confirmBatchRead(BatchId(fakeBatchId), fakeRemovalReason, false) + + // Then + verify(mockGrantedPersistenceStrategy).unlockAndKeep(fakeBatchId) + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + // endregion + + // region Storage.dropAll + + @Test + fun `M drop both granted and pending W dropAll()`() { + // When + testedStorage.dropAll() + + // Then + verify(mockGrantedPersistenceStrategy).dropAll() + verify(mockPendingPersistenceStrategy).dropAll() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + // endregion + + // region TrackingConsentProviderCallback + + @Test + fun `M register as consent listener W init()`() { + // Then + verify(mockConsentProvider).registerCallback(testedStorage) + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M do nothing W onConsentUpdated() {not_granted to not_granted}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.NOT_GRANTED) + + // Then + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M do nothing W onConsentUpdated() {not_granted to pending}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.PENDING) + + // Then + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M do nothing W onConsentUpdated() {not_granted to granted}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.GRANTED) + + // Then + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M drop pending data W onConsentUpdated() {pending to not_granted}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.NOT_GRANTED) + + // Then + verify(mockPendingPersistenceStrategy).dropAll() + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M do nothing W onConsentUpdated() {pending to pending}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.PENDING) + + // Then + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M migrate data W onConsentUpdated() {pending to granted}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.GRANTED) + + // Then + verify(mockPendingPersistenceStrategy).migrateData(mockGrantedPersistenceStrategy) + verifyNoMoreInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M do nothing W onConsentUpdated() {granted to not_granted}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.NOT_GRANTED) + + // Then + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M do nothing W onConsentUpdated() {granted to pending}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.PENDING) + + // Then + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + @Test + fun `M do nothing W onConsentUpdated() {granted to granted}`() { + // When + testedStorage.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.GRANTED) + + // Then + verifyNoInteractions( + mockGrantedPersistenceStrategy, + mockPendingPersistenceStrategy, + mockInternalLogger + ) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/AsyncEventWriteScopeTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/AsyncEventWriteScopeTest.kt new file mode 100644 index 0000000000..abdf5db81a --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/AsyncEventWriteScopeTest.kt @@ -0,0 +1,124 @@ +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class AsyncEventWriteScopeTest { + + private lateinit var testedScope: EventWriteScope + + @Mock + lateinit var mockEventBatchWriter: EventBatchWriter + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockExecutor: Executor + + @StringForgery + lateinit var fakeFeatureName: String + + private val fakeWriteLock = Any() + + @BeforeEach + fun `set up`() { + whenever(mockExecutor.execute(any())) doAnswer { + it.getArgument(0) + .run() + } + + testedScope = + AsyncEventWriteScope(mockExecutor, mockEventBatchWriter, fakeWriteLock, fakeFeatureName, mockInternalLogger) + } + + @Test + fun `M invoke with writer W invoke()`() { + // Given + val mockCallback = mock<(EventBatchWriter) -> Unit>() + + // When + testedScope.invoke(mockCallback) + + // Then + verify(mockCallback).invoke(mockEventBatchWriter) + } + + @Test + fun `M do sequential metadata write W invoke() { multithreaded }`( + @IntForgery(min = 2, max = 10) threadsCount: Int, + @Forgery fakeEventType: EventType, + forge: Forge + ) { + // Given + val executor = Executors.newFixedThreadPool(threadsCount) + var accumulator: Byte = 0 + val event = forge.aString().toByteArray() + // each write operation is going to increase value in meta by 1 + // in the end if some write operation was parallel to another, total number in meta + // won't be equal to the number of threads + // if write operations are parallel, there is a chance that there will be a conflict + // updating the meta (applying different updates to the same original state). + val callback: (EventBatchWriter) -> Unit = { + val value = it.currentMetadata()?.first() ?: 0 + it.write( + event = RawBatchEvent(data = event), + batchMetadata = byteArrayOf((value + 1).toByte()), + eventType = fakeEventType + ) + } + + whenever(mockEventBatchWriter.currentMetadata()) doAnswer { byteArrayOf(accumulator) } + whenever( + mockEventBatchWriter.write(any(), any(), any()) + ) doAnswer { + val value = it.getArgument(1).first() + accumulator = value + true + } + + // When + repeat(threadsCount) { + AsyncEventWriteScope(executor, mockEventBatchWriter, fakeWriteLock, fakeFeatureName, mockInternalLogger) + .invoke(callback) + } + executor.shutdown() + executor.awaitTermination(1, TimeUnit.SECONDS) + + // Then + assertThat(accumulator).isEqualTo(threadsCount.toByte()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/BatchDataTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/BatchDataTest.kt new file mode 100644 index 0000000000..64bb9dce21 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/BatchDataTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.ObjectTest +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions(ExtendWith(ForgeExtension::class)) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class BatchDataTest : ObjectTest() { + override fun createInstance(forge: Forge): BatchData { + return forge.getForgery() + } + + override fun createEqualInstance(source: BatchData, forge: Forge): BatchData { + return BatchData(source.id, source.data, source.metadata) + } + + override fun createUnequalInstance(source: BatchData, forge: Forge): BatchData? { + return forge.getForgery() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/ConsentAwareStorageTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/ConsentAwareStorageTest.kt new file mode 100644 index 0000000000..8c8b0e8af1 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/ConsentAwareStorageTest.kt @@ -0,0 +1,708 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.data.upload.DataOkHttpUploader.Companion.HTTP_ACCEPTED +import com.datadog.android.core.internal.metrics.BenchmarkUploads +import com.datadog.android.core.internal.metrics.MetricsDispatcher +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.RejectedExecutionException + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ConsentAwareStorageTest { + + private lateinit var testedStorage: Storage + + @Mock + lateinit var mockPendingOrchestrator: FileOrchestrator + + @Mock + lateinit var mockGrantedOrchestrator: FileOrchestrator + + @Mock + lateinit var mockBatchReaderWriter: BatchFileReaderWriter + + @Mock + lateinit var mockMetaReaderWriter: FileReaderWriter + + @Mock + lateinit var mockFileMover: FileMover + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockBenchmarkUploads: BenchmarkUploads + + @Mock + lateinit var mockFilePersistenceConfig: FilePersistenceConfig + + @Mock + lateinit var mockMetricsDispatcher: MetricsDispatcher + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @Forgery + lateinit var fakeEventType: EventType + + @StringForgery(StringForgeryType.ALPHABETICAL) + lateinit var fakeRootDirName: String + + @Forgery + lateinit var mockPendingRootParentFile: File + + @Forgery + lateinit var mockGrantedRootParentFile: File + + @StringForgery + lateinit var fakeFeatureName: String + + @IntForgery(min = 0, max = 100) + var fakePendingBatches: Int = 0 + + @BeforeEach + fun `set up`() { + whenever(mockPendingOrchestrator.getRootDir()) doReturn File(mockPendingRootParentFile, fakeRootDirName) + whenever(mockGrantedOrchestrator.getRootDir()) doReturn File(mockGrantedRootParentFile, fakeRootDirName) + whenever(mockPendingOrchestrator.getRootDirName()) doReturn fakeRootDirName + whenever(mockGrantedOrchestrator.getRootDirName()) doReturn fakeRootDirName + whenever((mockGrantedOrchestrator).decrementAndGetPendingFilesCount()) + .thenReturn(fakePendingBatches - 1) + whenever((mockPendingOrchestrator).decrementAndGetPendingFilesCount()) + .thenReturn(fakePendingBatches - 1) + + testedStorage = ConsentAwareStorage( + // same thread executor + executorService = FakeSameThreadExecutorService(), + grantedOrchestrator = mockGrantedOrchestrator, + pendingOrchestrator = mockPendingOrchestrator, + batchEventsReaderWriter = mockBatchReaderWriter, + batchMetadataReaderWriter = mockMetaReaderWriter, + fileMover = mockFileMover, + internalLogger = mockInternalLogger, + filePersistenceConfig = mockFilePersistenceConfig, + metricsDispatcher = mockMetricsDispatcher, + featureName = fakeFeatureName + ) + } + + // region getEventWriteScope + + @Test + fun `M provide writer W getEventWriteScope() {consent=granted}`() { + // Given + val mockCallback = mock<(EventBatchWriter) -> Unit>() + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.GRANTED) + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockCallback) + + // Then + argumentCaptor { + verify(mockCallback).invoke(capture()) + assertThat(firstValue).isInstanceOf(FileEventBatchWriter::class.java) + } + verifyNoMoreInteractions( + mockGrantedOrchestrator, + mockPendingOrchestrator, + mockBatchReaderWriter, + mockMetaReaderWriter + ) + } + + @Test + fun `M provide writer W getEventWriteScope() {consent=pending}`() { + // Given + val mockCallback = mock<(EventBatchWriter) -> Unit>() + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.PENDING) + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockCallback) + + // Then + argumentCaptor { + verify(mockCallback).invoke(capture()) + assertThat(firstValue).isInstanceOf(FileEventBatchWriter::class.java) + } + verifyNoMoreInteractions( + mockGrantedOrchestrator, + mockPendingOrchestrator, + mockBatchReaderWriter, + mockMetaReaderWriter + ) + } + + @Test + fun `M provide no-op writer W getEventWriteScope() {not_granted}`() { + // Given + val mockCallback = mock<(EventBatchWriter) -> Unit>() + fakeDatadogContext = fakeDatadogContext.copy(trackingConsent = TrackingConsent.NOT_GRANTED) + + // When + testedStorage.getEventWriteScope(fakeDatadogContext) + .invoke(mockCallback) + + // Then + argumentCaptor { + verify(mockCallback).invoke(capture()) + assertThat(firstValue).isInstanceOf(NoOpEventBatchWriter::class.java) + } + verifyNoMoreInteractions( + mockGrantedOrchestrator, + mockPendingOrchestrator, + mockBatchReaderWriter, + mockMetaReaderWriter + ) + } + + // endregion + + @Test + fun `M log error W getEventWriteScope() { task was rejected }`() { + // Given + val mockExecutor = mock() + whenever(mockExecutor.execute(any())) doThrow RejectedExecutionException() + testedStorage = ConsentAwareStorage( + // same thread executor + executorService = mockExecutor, + grantedOrchestrator = mockGrantedOrchestrator, + pendingOrchestrator = mockPendingOrchestrator, + batchEventsReaderWriter = mockBatchReaderWriter, + batchMetadataReaderWriter = mockMetaReaderWriter, + fileMover = mockFileMover, + internalLogger = mockInternalLogger, + filePersistenceConfig = mockFilePersistenceConfig, + metricsDispatcher = mockMetricsDispatcher, + featureName = fakeFeatureName + ) + + // When + testedStorage.getEventWriteScope(fakeDatadogContext).invoke { + // no-op + } + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule eventWriteScopeInvoke-$fakeFeatureName task on the executor", + RejectedExecutionException::class.java, + false + ) + verifyNoMoreInteractions( + mockGrantedOrchestrator, + mockPendingOrchestrator, + mockBatchReaderWriter, + mockMetaReaderWriter + ) + } + + // region readNextBatch + + @Test + fun `M provide batchData W readNextBatch()`( + @Forgery fakeData: List, + @StringForgery metadata: String, + @Forgery batchFile: File + ) { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(any())) doReturn batchFile + whenever(mockBatchReaderWriter.readData(batchFile)) doReturn fakeData + + val mockMetaFile = mock().apply { + whenever(exists()) doReturn true + } + + whenever(mockGrantedOrchestrator.getMetadataFile(batchFile)) doReturn mockMetaFile + val mockMetadata = metadata.toByteArray() + whenever(mockMetaReaderWriter.readData(mockMetaFile)) doReturn mockMetadata + + // Whenever + val batchData = testedStorage.readNextBatch() + + // Then + assertThat(batchData).isNotNull + assertThat(batchData?.id).isNotNull + assertThat(batchData?.data).isEqualTo(fakeData) + assertThat(batchData?.metadata).isEqualTo(mockMetadata) + } + + @Test + fun `M provide batchData W readNextBatch() { no metadata file provided }`( + @Forgery fakeData: List, + @Forgery batchFile: File + ) { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(any())) doReturn batchFile + whenever(mockBatchReaderWriter.readData(batchFile)) doReturn fakeData + whenever(mockGrantedOrchestrator.getMetadataFile(batchFile)) doReturn null + + // Whenever + val batchData = testedStorage.readNextBatch() + + // Then + assertThat(batchData).isNotNull + assertThat(batchData?.id).isNotNull + assertThat(batchData?.data).isEqualTo(fakeData) + assertThat(batchData?.metadata).isNull() + } + + @Test + fun `M provide batchData W readNextBatch() { metadata file doesn't exist }`( + @Forgery fakeData: List, + @Forgery batchFile: File + ) { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(any())) doReturn batchFile + whenever(mockBatchReaderWriter.readData(batchFile)) doReturn fakeData + + val mockMetaFile = mock().apply { + whenever(exists()) doReturn false + } + + whenever(mockGrantedOrchestrator.getMetadataFile(batchFile)) doReturn mockMetaFile + + // Whenever + val batchData = testedStorage.readNextBatch() + + // Then + assertThat(batchData).isNotNull + assertThat(batchData?.id).isNotNull + assertThat(batchData?.data).isEqualTo(fakeData) + assertThat(batchData?.metadata).isNull() + } + + @Test + fun `M return null no batch available W readNextBatch() {no file}`() { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(any())) doReturn null + + // Whenever + val batchData = testedStorage.readNextBatch() + + // Then + assertThat(batchData).isNull() + } + + // endregion + + // region confirmBatchRead + + @Test + fun `M read batch twice if released W readNextBatch()+confirmBatchRead() {delete=false}`( + @Forgery reason: RemovalReason, + @Forgery file: File + ) { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(emptySet())) doReturn file + val mockMetaFile: File = mock() + whenever(mockMetaFile.exists()) doReturn true + whenever(mockGrantedOrchestrator.getMetadataFile(file)) doReturn mockMetaFile + whenever(mockGrantedOrchestrator.getReadableFile(setOf(file))) doReturn null + + // When + val batchData1 = testedStorage.readNextBatch() + testedStorage.confirmBatchRead(batchData1!!.id, reason, false) + val batchData2 = testedStorage.readNextBatch() + + // Then + verify(mockFileMover, never()).delete(file) + verify(mockFileMover, never()).delete(mockMetaFile) + assertThat(batchData1).isEqualTo(batchData2) + } + + @Test + fun `M delete batch files W readNextBatch()+confirmBatchRead() {delete=true}`( + @Forgery file: File, + @Forgery reason: RemovalReason, + @StringForgery fakeMetaFilePath: String + ) { + // Given + testedStorage = ConsentAwareStorage( + executorService = FakeSameThreadExecutorService(), + grantedOrchestrator = mockGrantedOrchestrator, + pendingOrchestrator = mockPendingOrchestrator, + batchEventsReaderWriter = mockBatchReaderWriter, + batchMetadataReaderWriter = mockMetaReaderWriter, + fileMover = mockFileMover, + internalLogger = mockInternalLogger, + filePersistenceConfig = mockFilePersistenceConfig, + metricsDispatcher = mockMetricsDispatcher, + featureName = fakeFeatureName + ) + + whenever(mockGrantedOrchestrator.getReadableFile(emptySet())) doReturn file + val mockMetaFile: File = mock() + whenever(mockMetaFile.exists()) doReturn true + whenever(mockMetaFile.path) doReturn fakeMetaFilePath + whenever(mockGrantedOrchestrator.getMetadataFile(file)) doReturn mockMetaFile + whenever(mockFileMover.delete(file)) doReturn true + doReturn(true).whenever(mockFileMover).delete(mockMetaFile) + + // When + val batchData1 = testedStorage.readNextBatch() + testedStorage.confirmBatchRead(batchData1!!.id, reason, true) + testedStorage.readNextBatch() + + // Then + verify(mockFileMover).delete(file) + verify(mockFileMover).delete(mockMetaFile) + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(file), + eq(reason), + eq(fakePendingBatches - 1) + ) + } + + @Test + fun `M keep batch file locked W readNextBatch()+confirmBatchRead() {delete=true, != batchId}`( + @Forgery file: File, + @Forgery reason: RemovalReason, + @Forgery anotherFile: File + ) { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(emptySet())) doReturn file + val mockMetaFile: File = mock() + whenever(mockMetaFile.exists()) doReturn true + whenever(mockGrantedOrchestrator.getMetadataFile(file)) doReturn mockMetaFile + + // When + testedStorage.readNextBatch() + testedStorage.confirmBatchRead(BatchId.fromFile(anotherFile), reason, false) + testedStorage.readNextBatch() + + // Then + verify(mockFileMover, never()).delete(file) + verify(mockFileMover, never()).delete(mockMetaFile) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M warn W readNextBatch() + confirmBatchRead() {delete batch fails}`( + @Forgery reason: RemovalReason, + @Forgery file: File + ) { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(emptySet())) doReturn file + whenever(mockFileMover.delete(file)) doReturn false + + // When + val batchData1 = testedStorage.readNextBatch() + testedStorage.confirmBatchRead(batchData1!!.id, reason, true) + testedStorage.readNextBatch() + + // Then + verify(mockFileMover).delete(file) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + ConsentAwareStorage.WARNING_DELETE_FAILED.format(Locale.US, file.path) + ) + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M warn W readNextBatch() + confirmBatchRead() {delete batch meta fails}`( + @Forgery reason: RemovalReason, + @Forgery file: File, + @StringForgery fakeMetaFilePath: String + ) { + // Given + whenever(mockGrantedOrchestrator.getReadableFile(emptySet())) doReturn file + val mockMetaFile: File = mock() + whenever(mockMetaFile.exists()) doReturn true + whenever(mockMetaFile.path) doReturn fakeMetaFilePath + whenever(mockGrantedOrchestrator.getMetadataFile(file)) doReturn mockMetaFile + whenever(mockFileMover.delete(file)) doReturn true + doReturn(false).whenever(mockFileMover).delete(mockMetaFile) + + // When + val batchData1 = testedStorage.readNextBatch() + testedStorage.confirmBatchRead(batchData1!!.id, reason, true) + + // Then + verify(mockFileMover).delete(file) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + ConsentAwareStorage.WARNING_DELETE_FAILED.format(Locale.US, mockMetaFile.path) + ) + verifyNoMoreInteractions(mockInternalLogger) + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(file), + eq(reason), + eq(fakePendingBatches - 1) + ) + } + + // endregion + + // region dropAll + + @Test + fun `M delete everything W dropAll()`( + @Forgery pendingFile: File, + @Forgery grantedFile: File, + @StringForgery fakePendingMetaFilePath: String, + @StringForgery fakeGrantedMetaFilePath: String + ) { + // Given + testedStorage = ConsentAwareStorage( + executorService = FakeSameThreadExecutorService(), + grantedOrchestrator = mockGrantedOrchestrator, + pendingOrchestrator = mockPendingOrchestrator, + batchEventsReaderWriter = mockBatchReaderWriter, + batchMetadataReaderWriter = mockMetaReaderWriter, + fileMover = mockFileMover, + internalLogger = mockInternalLogger, + filePersistenceConfig = mockFilePersistenceConfig, + metricsDispatcher = mockMetricsDispatcher, + featureName = fakeFeatureName + ) + + whenever(mockGrantedOrchestrator.getAllFiles()) doReturn listOf(grantedFile) + whenever(mockPendingOrchestrator.getAllFiles()) doReturn listOf(pendingFile) + val mockPendingMetaFile: File = mock() + whenever(mockPendingMetaFile.exists()) doReturn true + whenever(mockPendingMetaFile.path) doReturn fakePendingMetaFilePath + val mockGrantedMetaFile: File = mock() + whenever(mockGrantedMetaFile.exists()) doReturn true + whenever(mockGrantedMetaFile.path) doReturn fakeGrantedMetaFilePath + whenever(mockGrantedOrchestrator.getMetadataFile(grantedFile)) doReturn mockGrantedMetaFile + whenever(mockFileMover.delete(grantedFile)) doReturn true + whenever(mockPendingOrchestrator.getMetadataFile(pendingFile)) doReturn mockPendingMetaFile + whenever(mockFileMover.delete(pendingFile)) doReturn true + doReturn(true).whenever(mockFileMover).delete(mockGrantedMetaFile) + doReturn(true).whenever(mockFileMover).delete(mockPendingMetaFile) + + // When + testedStorage.dropAll() + + // Then + verify(mockFileMover).delete(grantedFile) + verify(mockFileMover).delete(mockGrantedMetaFile) + verify(mockFileMover).delete(pendingFile) + verify(mockFileMover).delete(mockPendingMetaFile) + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(grantedFile), + argThat { + this is RemovalReason.Flushed + }, + eq(fakePendingBatches - 1) + ) + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(pendingFile), + argThat { + this is RemovalReason.Flushed + }, + eq(fakePendingBatches - 1) + ) + } + + @Test + fun `M delete everything W dropAll() { there are locked batches }`( + @Forgery files: List, + forge: Forge + ) { + // Given + testedStorage = ConsentAwareStorage( + executorService = FakeSameThreadExecutorService(), + grantedOrchestrator = mockGrantedOrchestrator, + pendingOrchestrator = mockPendingOrchestrator, + batchEventsReaderWriter = mockBatchReaderWriter, + batchMetadataReaderWriter = mockMetaReaderWriter, + fileMover = mockFileMover, + internalLogger = mockInternalLogger, + filePersistenceConfig = mockFilePersistenceConfig, + metricsDispatcher = mockMetricsDispatcher, + featureName = fakeFeatureName + ) + + whenever(mockGrantedOrchestrator.getReadableFile(any())) doReturnConsecutively files + val mockMetaFiles = files.map { + val fakeMetaFilePath = forge.anAlphabeticalString() + val mockMetaFile = mock() + whenever(mockMetaFile.exists()) doReturn true + whenever(mockMetaFile.path) doReturn fakeMetaFilePath + whenever(mockGrantedOrchestrator.getMetadataFile(it)) doReturn mockMetaFile + doReturn(true).whenever(mockFileMover).delete(mockMetaFile) + mockMetaFile + } + files.forEach { + whenever(mockFileMover.delete(it)) doReturn true + } + + // ConcurrentModificationException is thrown only during the next check after remove, + // so to make sure it is not thrown we need at least 2 locked batches + repeat(files.size) { + testedStorage.readNextBatch() + } + + // When + testedStorage.dropAll() + + // Then + files.forEachIndexed { index, file -> + verify(mockFileMover).delete(file) + verify(mockFileMover).delete(mockMetaFiles[index]) + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(file), + argThat { + this is RemovalReason.Flushed + }, + eq(fakePendingBatches - 1) + ) + } + } + + // endregion + + // region benchmarking + + @Test + fun `M send benchmark for bytes deleted W confirmBatchRead { HTTP_ACCEPTED }`( + @LongForgery(1, 1000) fakeFileSize: Long, + @StringForgery fakePath: String, + @Mock mockFile: File + ) { + // Given + testedStorage = ConsentAwareStorage( + // same thread executor + executorService = FakeSameThreadExecutorService(), + grantedOrchestrator = mockGrantedOrchestrator, + pendingOrchestrator = mockPendingOrchestrator, + batchEventsReaderWriter = mockBatchReaderWriter, + batchMetadataReaderWriter = mockMetaReaderWriter, + fileMover = mockFileMover, + internalLogger = mockInternalLogger, + filePersistenceConfig = mockFilePersistenceConfig, + metricsDispatcher = mockMetricsDispatcher, + featureName = fakeFeatureName, + benchmarkUploads = mockBenchmarkUploads + ) + + whenever(mockFileMover.delete(mockFile)).thenReturn(true) + whenever(mockFile.absolutePath) doReturn fakePath + whenever(mockGrantedOrchestrator.getReadableFile(emptySet())) doReturn mockFile + val mockMetaFile: File = mock() + whenever(mockMetaFile.exists()) doReturn true + whenever(mockGrantedOrchestrator.getMetadataFile(mockFile)) doReturn mockMetaFile + whenever(mockGrantedOrchestrator.getReadableFile(setOf(mockFile))) doReturn null + val batchData1 = testedStorage.readNextBatch() + whenever(mockFile.length()).thenReturn(fakeFileSize) + + // When + testedStorage.confirmBatchRead( + batchId = batchData1!!.id, + removalReason = RemovalReason.IntakeCode(HTTP_ACCEPTED), + deleteBatch = true + ) + + // Then + verify(mockBenchmarkUploads).sendBenchmarkBytesDeleted(any(), any()) + } + + @Test + fun `M not send benchmark for bytes deleted bytesDeleted W confirmBatchRead { OBSOLETE }`( + @LongForgery(1, 1000) fakeFileSize: Long, + @StringForgery fakePath: String, + @Mock mockFile: File + ) { + // Given + testedStorage = ConsentAwareStorage( + // same thread executor + executorService = FakeSameThreadExecutorService(), + grantedOrchestrator = mockGrantedOrchestrator, + pendingOrchestrator = mockPendingOrchestrator, + batchEventsReaderWriter = mockBatchReaderWriter, + batchMetadataReaderWriter = mockMetaReaderWriter, + fileMover = mockFileMover, + internalLogger = mockInternalLogger, + filePersistenceConfig = mockFilePersistenceConfig, + metricsDispatcher = mockMetricsDispatcher, + featureName = fakeFeatureName, + benchmarkUploads = mockBenchmarkUploads + ) + + whenever(mockFileMover.delete(mockFile)).thenReturn(true) + whenever(mockFile.absolutePath) doReturn fakePath + whenever(mockGrantedOrchestrator.getReadableFile(emptySet())) doReturn mockFile + val mockMetaFile: File = mock() + whenever(mockMetaFile.exists()) doReturn true + whenever(mockGrantedOrchestrator.getMetadataFile(mockFile)) doReturn mockMetaFile + whenever(mockGrantedOrchestrator.getReadableFile(setOf(mockFile))) doReturn null + val batchData1 = testedStorage.readNextBatch() + whenever(mockFile.length()).thenReturn(fakeFileSize) + + // When + testedStorage.confirmBatchRead( + batchId = batchData1!!.id, + removalReason = RemovalReason.Obsolete, + deleteBatch = true + ) + + // Then + verify(mockBenchmarkUploads, never()).sendBenchmarkBytesDeleted(any(), any()) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/FakeSameThreadExecutorService.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/FakeSameThreadExecutorService.kt new file mode 100644 index 0000000000..b4306f900c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/FakeSameThreadExecutorService.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.FutureTask +import java.util.concurrent.TimeUnit + +internal class FakeSameThreadExecutorService : AbstractExecutorService() { + + private var isShutdown = false + + override fun execute(command: Runnable?) { + if (command is FutureTask<*>) { + command.run() + val result = command.get() + println("Command returned $result") + } else { + command?.run() + } + } + + override fun shutdown() { + isShutdown = true + } + + override fun shutdownNow(): MutableList { + isShutdown = true + return mutableListOf() + } + + override fun isShutdown(): Boolean = isShutdown + + override fun isTerminated(): Boolean = isShutdown + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { + return true + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/FileEventBatchWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/FileEventBatchWriterTest.kt new file mode 100644 index 0000000000..88b81c3459 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/FileEventBatchWriterTest.kt @@ -0,0 +1,415 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.FileEventBatchWriter.Companion.ERROR_LARGE_DATA +import com.datadog.android.core.internal.persistence.FileEventBatchWriter.Companion.NO_BATCH_FILE_AVAILABLE +import com.datadog.android.core.internal.persistence.FileEventBatchWriter.Companion.WARNING_METADATA_WRITE_FAILED +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.FileWriter +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class FileEventBatchWriterTest { + + private lateinit var testedWriter: EventBatchWriter + + @Mock + lateinit var mockBatchWriter: FileWriter + + @Mock + lateinit var mockMetaReaderWriter: FileReaderWriter + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockFilePersistenceConfig: FilePersistenceConfig + + @Mock + lateinit var mockBatchWriteEventListener: BatchWriteEventListener + + @Mock + lateinit var mockFileOrchestrator: FileOrchestrator + + @Forgery + lateinit var fakeBatchFile: File + + @Forgery + lateinit var fakeBatchMetadataFile: File + + @Forgery + lateinit var fakeEventType: EventType + + @BeforeEach + fun `set up`() { + testedWriter = FileEventBatchWriter( + fileOrchestrator = mockFileOrchestrator, + eventsWriter = mockBatchWriter, + metadataReaderWriter = mockMetaReaderWriter, + filePersistenceConfig = mockFilePersistenceConfig, + internalLogger = mockInternalLogger, + batchWriteEventListener = mockBatchWriteEventListener + ) + whenever(mockFilePersistenceConfig.maxItemSize) doReturn Long.MAX_VALUE + whenever(mockFileOrchestrator.getWritableFile()) doReturn fakeBatchFile + whenever(mockFileOrchestrator.getMetadataFile(fakeBatchFile)) doReturn fakeBatchMetadataFile + } + + // region write + + @Test + fun `M write event W write()`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + val serializedMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + whenever(mockMetaReaderWriter.readData(fakeBatchMetadataFile)) doReturn serializedMetadata + whenever( + mockBatchWriter.writeData(fakeBatchFile, batchEvent, true) + ) doReturn true + + // When + val result = testedWriter.write(batchEvent, serializedMetadata, fakeEventType) + + // Then + assertThat(result).isTrue() + + verify(mockBatchWriter).writeData( + fakeBatchFile, + batchEvent, + append = true + ) + verify(mockMetaReaderWriter).writeData( + fakeBatchMetadataFile, + serializedMetadata, + append = false + ) + + verifyNoMoreInteractions( + mockBatchWriter, + mockMetaReaderWriter + ) + } + + @Test + fun `M do nothing W write() {empty array}`( + @StringForgery batchMetadata: String + ) { + // Given + val rawBatchEvent = RawBatchEvent(data = ByteArray(0)) + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + + // When + val result = testedWriter.write(rawBatchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isTrue + + verifyNoInteractions( + mockBatchWriter, + mockMetaReaderWriter + ) + } + + @Test + fun `M return false W write() {batch file cannot be allocated}`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + whenever(mockFileOrchestrator.getWritableFile()) doReturn null + + // When + val result = testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isFalse + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + NO_BATCH_FILE_AVAILABLE + ) + } + + @Test + fun `M return false W write() {item is too big}`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + val maxItemSize = batchEvent.data.size - 1 + whenever(mockFilePersistenceConfig.maxItemSize) doReturn maxItemSize.toLong() + + // When + val result = testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isFalse + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + ERROR_LARGE_DATA.format( + Locale.US, + batchEvent.data.size, + maxItemSize + ) + ) + } + + @Test + fun `M return false W write() {write operation failed}`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String, + @Forgery batchFile: File + ) { + // Given + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + whenever(mockBatchWriter.writeData(batchFile, batchEvent, true)) doReturn false + + // When + val result = testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isFalse + } + + @Test + fun `M not write metadata W write() {no available file}`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + whenever(mockFileOrchestrator.getMetadataFile(fakeBatchFile)) doReturn null + + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + + whenever(mockBatchWriter.writeData(fakeBatchFile, batchEvent, true)) doReturn true + + // When + val result = testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isTrue + + verifyNoInteractions(mockMetaReaderWriter) + } + + @Test + fun `M not write metadata W write() {null or empty metadata}`( + @Forgery batchEvent: RawBatchEvent, + forge: Forge + ) { + // Given + whenever(mockBatchWriter.writeData(fakeBatchFile, batchEvent, true)) doReturn true + + // When + val result = testedWriter.write(batchEvent, forge.aNullable { ByteArray(0) }, fakeEventType) + + // Then + assertThat(result).isTrue + + verify(mockBatchWriter).writeData( + fakeBatchFile, + batchEvent, + true + ) + verifyNoMoreInteractions(mockBatchWriter) + verifyNoInteractions(mockMetaReaderWriter) + } + + @Test + fun `M not write metadata W write() {item is too big}`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + val maxItemSize = batchEvent.data.size - 1 + whenever(mockFilePersistenceConfig.maxItemSize) doReturn maxItemSize.toLong() + + // When + val result = testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isFalse + + verifyNoInteractions(mockMetaReaderWriter) + } + + @Test + fun `M not write metadata W write() {write operation failed}`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + whenever( + mockBatchWriter.writeData(fakeBatchFile, batchEvent, true) + ) doReturn false + + // When + val result = testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isFalse + + verifyNoInteractions(mockMetaReaderWriter) + } + + @Test + fun `M log warning W write() {write metadata failed}`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + whenever( + mockBatchWriter.writeData(fakeBatchFile, batchEvent, true) + ) doReturn true + whenever( + mockMetaReaderWriter.writeData(fakeBatchMetadataFile, serializedBatchMetadata, false) + ) doReturn false + + // When + val result = testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + assertThat(result).isTrue() + + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + WARNING_METADATA_WRITE_FAILED.format( + Locale.US, + fakeBatchMetadataFile + ) + ) + } + + // endregion + + // region currentMetadata + + @Test + fun `M not read metadata W currentMetadata() {no available file}`() { + // Given + whenever(mockFileOrchestrator.getMetadataFile(fakeBatchFile)) doReturn null + + // When + val meta = testedWriter.currentMetadata() + + // Then + assertThat(meta).isNull() + verifyNoInteractions(mockMetaReaderWriter) + } + + @Test + fun `M not read metadata W currentMetadata() { file doesn't exist }`() { + // Given + val fakeNonExistentMetaFile = mock().apply { + whenever(exists()) doReturn false + } + + whenever(mockFileOrchestrator.getMetadataFile(fakeBatchFile)) doReturn fakeNonExistentMetaFile + + // When + val meta = testedWriter.currentMetadata() + + // Then + assertThat(meta).isNull() + verifyNoInteractions(mockMetaReaderWriter) + } + + @Test + fun `M read metadata W currentMetadata()`( + @StringForgery fakeMetadata: String, + @TempDir fakeMetadataDir: File, + forge: Forge + ) { + // Given + val fakeMetaFile = File(fakeMetadataDir, forge.anAlphabeticalString()) + fakeMetaFile.createNewFile() + + whenever(mockFileOrchestrator.getMetadataFile(fakeBatchFile)) doReturn fakeMetaFile + whenever(mockMetaReaderWriter.readData(fakeMetaFile)) doReturn + fakeMetadata.toByteArray() + + // When + val meta = testedWriter.currentMetadata() + + // Then + assertThat(meta).isEqualTo(fakeMetadata.toByteArray()) + } + + // endregion + + // region benchmark + + @Test + fun `M send onWriteEvent W write`( + @Forgery batchEvent: RawBatchEvent, + @StringForgery batchMetadata: String + ) { + // Given + val serializedBatchMetadata = batchMetadata.toByteArray(Charsets.UTF_8) + whenever( + mockBatchWriter.writeData(fakeBatchFile, batchEvent, true) + ) doReturn true + whenever( + mockMetaReaderWriter.writeData(fakeBatchMetadataFile, serializedBatchMetadata, false) + ) doReturn false + + // When + testedWriter.write(batchEvent, serializedBatchMetadata, fakeEventType) + + // Then + verify(mockBatchWriteEventListener).onWriteEvent(batchEvent.data.size.toLong()) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/JsonObjectDeserializerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/JsonObjectDeserializerTest.kt new file mode 100644 index 0000000000..1db283826d --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/JsonObjectDeserializerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.utils.forge.Configurator +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class JsonObjectDeserializerTest { + + private val testedDeserializer = JsonObjectDeserializer(internalLogger = mock()) + + @Test + fun `M deserialize a serialized JsonObject W deserialize()`( + @Forgery fakeJsonObject: JsonObject + ) { + // Given + val fakeModel = fakeJsonObject.toString() + + // When + val deserialized = testedDeserializer.deserialize(fakeModel) + + // Then + assertThat(deserialized.toString()).isEqualTo(fakeModel) + } + + @Test + fun `M return null W deserialize() { json array }`( + @Forgery fakeJsonObject: JsonObject + ) { + // Given + val fakeModel = "[$fakeJsonObject]" + + // When + val deserialized = testedDeserializer.deserialize(fakeModel) + + // Then + assertThat(deserialized).isNull() + } + + @Test + fun `M return null W deserialize() { malformed model }`( + @StringForgery fakeModel: String + ) { + // When + val deserialized = testedDeserializer.deserialize(fakeModel) + + // Then + assertThat(deserialized).isNull() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/NoOpEventBatchWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/NoOpEventBatchWriterTest.kt new file mode 100644 index 0000000000..739f265d12 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/NoOpEventBatchWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence + +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class NoOpEventBatchWriterTest { + + private lateinit var testedWriter: EventBatchWriter + + @Forgery + lateinit var fakeEventType: EventType + + @BeforeEach + fun `set up`() { + testedWriter = NoOpEventBatchWriter() + } + + @Test + fun `M return no metadata W currentMetadata()`() { + assertThat(testedWriter.currentMetadata()).isNull() + } + + @Test + fun `M notify about successful write W write()`( + @Forgery fakeData: RawBatchEvent, + @StringForgery fakeMetadata: String, + forge: Forge + ) { + // When + val result = testedWriter.write( + fakeData, + forge.aNullable { fakeMetadata.toByteArray() }, + fakeEventType + ) + + // Then + assertThat(result).isTrue + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandlerTest.kt new file mode 100644 index 0000000000..c6e0c2c7b0 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandlerTest.kt @@ -0,0 +1,179 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreReadCallback +import com.datadog.android.api.storage.datastore.DataStoreWriteCallback +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.ExecutorService + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataStoreFileHandlerTest { + + private lateinit var testedDataStoreHandler: DataStoreFileHandler + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockExecutorService: ExecutorService + + @Mock + lateinit var mockDataStoreFileReader: DatastoreFileReader + + @Mock + lateinit var mockDeserializer: Deserializer + + @Mock + lateinit var mockDatastoreFileWriter: DatastoreFileWriter + + @Mock + lateinit var mockSerializer: Serializer + + @Mock + lateinit var mockDataStoreWriteCallback: DataStoreWriteCallback + + @StringForgery + lateinit var fakeFeatureName: String + + @StringForgery + lateinit var fakeKey: String + + @StringForgery + lateinit var fakeDataString: String + + private lateinit var fileCallback: DataStoreReadCallback + + @BeforeEach + fun setup() { + whenever(mockExecutorService.execute(any())) doAnswer { + it.getArgument(0).run() + } + + fileCallback = object : DataStoreReadCallback { + override fun onSuccess(dataStoreContent: DataStoreContent?) {} + override fun onFailure() {} + } + + testedDataStoreHandler = DataStoreFileHandler( + executorService = mockExecutorService, + internalLogger = mockInternalLogger, + dataStoreFileReader = mockDataStoreFileReader, + datastoreFileWriter = mockDatastoreFileWriter + ) + } + + @Test + fun `M call dataStoreReader with version 0 W value() { default version }`() { + // When + testedDataStoreHandler.value( + key = fakeKey, + deserializer = mockDeserializer, + callback = fileCallback + ) + + // Then + verify(mockDataStoreFileReader).read( + key = fakeKey, + deserializer = mockDeserializer, + callback = fileCallback + ) + } + + @Test + fun `M call dataStoreReader W value()`( + @IntForgery fakeVersion: Int + ) { + // When + testedDataStoreHandler.value( + key = fakeKey, + deserializer = mockDeserializer, + version = fakeVersion, + callback = fileCallback + ) + + // Then + verify(mockDataStoreFileReader).read( + key = fakeKey, + deserializer = mockDeserializer, + version = fakeVersion, + callback = fileCallback + ) + } + + @Test + fun `M call dataStoreWriter with version 0 W setValue()`( + @IntForgery fakeVersion: Int + ) { + // When + testedDataStoreHandler.setValue( + key = fakeKey, + data = fakeDataString, + version = fakeVersion, + callback = mockDataStoreWriteCallback, + serializer = mockSerializer + ) + + // Then + verify(mockDatastoreFileWriter).write( + key = fakeKey, + data = fakeDataString, + serializer = mockSerializer, + callback = mockDataStoreWriteCallback, + version = fakeVersion + ) + } + + @Test + fun `M call dataStoreWriter W removeValue()`() { + // When + testedDataStoreHandler.removeValue( + key = fakeKey, + callback = mockDataStoreWriteCallback + ) + + // Then + verify(mockDatastoreFileWriter).delete( + key = fakeKey, + callback = mockDataStoreWriteCallback + ) + } + + @Test + fun `M call dataStoreWriter W clearAll()`() { + // When + testedDataStoreHandler.clearAllData() + + // Then + verify(mockDatastoreFileWriter).clearAllData() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelperTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelperTest.kt new file mode 100644 index 0000000000..5d3a911b48 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelperTest.kt @@ -0,0 +1,74 @@ +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataStoreFileHelperTest { + + lateinit var testedHelper: DataStoreFileHelper + + @TempDir + lateinit var tempDir: File + + @StringForgery + lateinit var fakeFeatureName: String + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedHelper = DataStoreFileHelper(mockInternalLogger) + } + + @Test + fun `M return datastore file W getDataStoreFile()`( + @StringForgery fakeKey: String + ) { + // Given + val expectedDataStoreDir = File(tempDir, "datastore_v0") + val expectedFeatureDir = File(expectedDataStoreDir, fakeFeatureName) + val expectedFile = File(expectedFeatureDir, fakeKey) + + // When + val result = testedHelper.getDataStoreFile(tempDir, fakeFeatureName, fakeKey) + + // Then + assertThat(result).isEqualTo(expectedFile) + assertThat(result.parentFile).exists().canWrite() + } + + @Test + fun `M return datastore dir W getDataStoreDirectory()`() { + // Given + val expectedDataStoreDir = File(tempDir, "datastore_v0") + val expectedFeatureDir = File(expectedDataStoreDir, fakeFeatureName) + + // When + val result = testedHelper.getDataStoreDirectory(tempDir, fakeFeatureName) + + // Then + assertThat(result).isEqualTo(expectedFeatureDir) + assertThat(result).exists().canWrite() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileReaderTest.kt new file mode 100644 index 0000000000..0f02d24abd --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileReaderTest.kt @@ -0,0 +1,282 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreReadCallback +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader.Companion.INVALID_NUMBER_OF_BLOCKS_ERROR +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader.Companion.UNEXPECTED_BLOCKS_ORDER_ERROR +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.nullableArgumentCaptor +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.nio.ByteBuffer +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataStoreFileReaderTest { + private lateinit var testedDatastoreFileReader: DatastoreFileReader + + @Mock + lateinit var mockDataStoreFileHelper: DataStoreFileHelper + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockTLVBlockFileReader: TLVBlockFileReader + + @Mock + lateinit var mockDataStoreFile: File + + @TempDir + lateinit var mockStorageDir: File + + @Mock + lateinit var mockDataStoreDirectory: File + + @Mock + lateinit var mockDeserializer: Deserializer + + @StringForgery + lateinit var fakeFeatureName: String + + @StringForgery + lateinit var fakeDataString: String + + @StringForgery + lateinit var fakeKey: String + + private lateinit var fakeDataBytes: ByteArray + private lateinit var versionBlock: TLVBlock + private lateinit var dataBlock: TLVBlock + private lateinit var blocksReturned: ArrayList + + @BeforeEach + fun setup() { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + + whenever( + mockDataStoreFileHelper.getDataStoreFile( + storageDir = eq(mockStorageDir), + featureName = eq(fakeFeatureName), + key = any() + ) + ).thenReturn(mockDataStoreFile) + + whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockDeserializer.deserialize(fakeDataString)).thenReturn(fakeDataBytes) + + versionBlock = createVersionBlock(true) + dataBlock = createDataBlock() + blocksReturned = arrayListOf(versionBlock, dataBlock) + whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) + + testedDatastoreFileReader = DatastoreFileReader( + dataStoreFileHelper = mockDataStoreFileHelper, + featureName = fakeFeatureName, + internalLogger = mockInternalLogger, + storageDir = mockStorageDir, + tlvBlockFileReader = mockTLVBlockFileReader + ) + } + + @Test + fun `M return no data W read() { datastore file does not exist }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + val mockCallback = mock>() + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + callback = mockCallback + ) + + // Then + nullableArgumentCaptor> { + verify(mockCallback).onSuccess(capture()) + assertThat(lastValue).isNull() + verifyNoMoreInteractions(mockCallback) + } + } + + @Test + fun `M log error W read() { invalid number of blocks }`() { + // Given + blocksReturned.removeAt(blocksReturned.lastIndex) + + val foundBlocks = blocksReturned.size + val expectedBlocks = TLVBlockType.values().size + val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, foundBlocks, expectedBlocks) + val mockCallback = mock>() + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + callback = mockCallback + ) + + // Then + verify(mockCallback).onFailure() + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = expectedError + ) + verifyNoMoreInteractions(mockCallback) + } + + @Test + fun `M return no data W value() { explicit version and versions don't match }`() { + // Given + val mockCallback = mock>() + + // When + testedDatastoreFileReader.read( + key = fakeKey, + version = 99, + callback = mockCallback, + deserializer = mockDeserializer + ) + + // Then + nullableArgumentCaptor> { + verify(mockCallback).onSuccess(capture()) + assertThat(lastValue).isNull() + verifyNoMoreInteractions(mockCallback) + } + } + + @Test + fun `M return deserialized data W read()`() { + // Given + blocksReturned.clear() + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createDataBlock()) + val mockCallback = mock>() + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + callback = mockCallback + ) + + // Then + nullableArgumentCaptor> { + verify(mockCallback).onSuccess(capture()) + assertThat(lastValue?.data).isEqualTo(fakeDataBytes) + verifyNoMoreInteractions(mockCallback) + } + } + + @Test + fun `M return onFailure W read() { invalid number of blocks }`() { + // Given + blocksReturned.removeAt(blocksReturned.lastIndex) + val expectedMessage = + INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, blocksReturned.size, TLVBlockType.values().size) + val mockCallback = mock>() + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + callback = mockCallback + ) + + // Then + verify(mockCallback).onFailure() + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = expectedMessage + ) + verifyNoMoreInteractions(mockCallback) + } + + @Test + fun `M log unexpectedBlocksOrder error W read() { unexpected block order }`() { + // Given + blocksReturned = arrayListOf(dataBlock, versionBlock) + whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) + val mockCallback = mock>() + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + callback = mockCallback + ) + + // Then + verify(mockCallback).onFailure() + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = UNEXPECTED_BLOCKS_ORDER_ERROR + ) + verifyNoMoreInteractions(mockCallback) + } + + private fun createVersionBlock(valid: Boolean, newVersion: Int = 0): TLVBlock { + return if (valid) { + TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion).array(), + internalLogger = mockInternalLogger + ) + } else { + TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion - 1).array(), + internalLogger = mockInternalLogger + ) + } + } + + private fun createDataBlock(dataBytes: ByteArray = fakeDataBytes): TLVBlock = + TLVBlock( + type = TLVBlockType.DATA, + data = dataBytes, + internalLogger = mockInternalLogger + ) +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileWriterTest.kt new file mode 100644 index 0000000000..88feeb65ff --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreWriteCallback +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileWriter.Companion.FAILED_TO_SERIALIZE_DATA_ERROR +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataStoreFileWriterTest { + private lateinit var testedDatastoreFileWriter: DatastoreFileWriter + + @Mock + lateinit var mockDataStoreFileHelper: DataStoreFileHelper + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockFileReaderWriter: FileReaderWriter + + @Mock + lateinit var mockSerializer: Serializer + + @Mock + lateinit var mockDataStoreWriteCallback: DataStoreWriteCallback + + @Mock + lateinit var mockDataStoreDirectory: File + + @Mock + lateinit var mockDataStoreFile: File + + @TempDir + lateinit var mockStorageDir: File + + @StringForgery + lateinit var fakeFeatureName: String + + @StringForgery + lateinit var fakeDataString: String + + @StringForgery + lateinit var fakeKey: String + + private lateinit var fakeDataBytes: ByteArray + + @BeforeEach + fun setup() { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + + whenever( + mockDataStoreFileHelper.getDataStoreFile( + storageDir = eq(mockStorageDir), + featureName = eq(fakeFeatureName), + key = any() + ) + ).thenReturn(mockDataStoreFile) + + whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(fakeDataString) + + testedDatastoreFileWriter = DatastoreFileWriter( + dataStoreFileHelper = mockDataStoreFileHelper, + featureName = fakeFeatureName, + internalLogger = mockInternalLogger, + storageDir = mockStorageDir, + fileReaderWriter = mockFileReaderWriter + ) + } + + @Test + fun `M not write to file W write() { unable to serialize data }`() { + // Given + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) + + // When + testedDatastoreFileWriter.write( + key = fakeKey, + serializer = mockSerializer, + data = fakeDataString, + callback = mockDataStoreWriteCallback, + version = 0 + ) + + // Then + verifyNoInteractions(mockFileReaderWriter) + } + + @Test + fun `M log error W write() { unable to serialize data }`() { + // Given + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) + + // When + testedDatastoreFileWriter.write( + key = fakeKey, + data = fakeDataString, + serializer = mockSerializer, + callback = mockDataStoreWriteCallback, + version = 0 + ) + + // Then + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = FAILED_TO_SERIALIZE_DATA_ERROR + ) + } + + @Test + fun `M write to file W setValue()`() { + // When + testedDatastoreFileWriter.write( + key = fakeKey, + data = fakeDataString, + serializer = mockSerializer, + callback = mockDataStoreWriteCallback, + version = 0 + ) + + // Then + verify(mockFileReaderWriter).writeData( + eq(mockDataStoreFile), + any(), + eq(false) + ) + } + + @Test + fun `M call deleteSafe W removeValue() { file exists }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + + // When + testedDatastoreFileWriter.delete(fakeKey, mockDataStoreWriteCallback) + + // Then + verify(mockDataStoreFile).deleteSafe(mockInternalLogger) + } + + @Test + fun `M not call deleteSafe W removeValue() { file does not exist }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + + // When + testedDatastoreFileWriter.delete(fakeKey, mockDataStoreWriteCallback) + + // Then + verify(mockDataStoreFile, never()).deleteSafe(mockInternalLogger) + } + + @Test + fun `M call deleteSafe W clearAllData() { for files }`() { + // Given + whenever( + mockDataStoreFileHelper.getDataStoreDirectory( + storageDir = mockStorageDir, + featureName = fakeFeatureName + ) + ).thenReturn(mockDataStoreDirectory) + + whenever(mockDataStoreDirectory.listFiles()) + .thenReturn(arrayOf(mockDataStoreFile)) + + // When + testedDatastoreFileWriter.clearAllData() + + // Then + verify(mockDataStoreFile).deleteSafe(mockInternalLogger) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/EncryptedFileReaderWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/EncryptedFileReaderWriterTest.kt new file mode 100644 index 0000000000..3a3c0ad225 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/EncryptedFileReaderWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import com.datadog.android.api.InternalLogger +import com.datadog.android.security.Encryption +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import kotlin.experimental.inv + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class EncryptedFileReaderWriterTest { + + @Mock + lateinit var mockEncryption: Encryption + + @Mock + lateinit var mockFileReaderWriterDelegate: FileReaderWriter + + @Mock + lateinit var mockFile: File + + @Mock + lateinit var mockInternalLogger: InternalLogger + + private lateinit var testedReaderWriter: EncryptedFileReaderWriter + + @BeforeEach + fun setUp() { + whenever(mockFileReaderWriterDelegate.writeData(any(), any(), any())) doReturn true + + whenever(mockEncryption.encrypt(any())) doAnswer { + val bytes = it.getArgument(0) + encrypt(bytes) + } + whenever(mockEncryption.decrypt(any())) doAnswer { + val bytes = it.getArgument(0) + decrypt(bytes) + } + + testedReaderWriter = EncryptedFileReaderWriter( + mockEncryption, + mockFileReaderWriterDelegate, + mockInternalLogger + ) + } + + // region EncryptedFileReaderWriter#writeData tests + + @Test + fun `M encrypt data and return true W writeData()`( + @StringForgery data: String + ) { + // When + val result = testedReaderWriter.writeData( + mockFile, + data.toByteArray(), + append = false + ) + val encryptedData = encrypt(data.toByteArray()) + + // Then + assertThat(result).isTrue() + verify(mockFileReaderWriterDelegate) + .writeData( + mockFile, + encryptedData, + false + ) + + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log internal error and return false W writeData() { bad encryption result }`( + @StringForgery data: String + ) { + // Given + whenever(mockEncryption.encrypt(data.toByteArray())) doReturn ByteArray(0) + + // When + val result = testedReaderWriter.writeData( + mockFile, + data.toByteArray(), + append = false + ) + + // Then + assertThat(result).isFalse() + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + EncryptedFileReaderWriter.BAD_ENCRYPTION_RESULT_MESSAGE + ) + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockFileReaderWriterDelegate) + } + + @Test + fun `M log internal error and return false W writeData() { append = true }`( + @StringForgery data: String + ) { + // When + val result = testedReaderWriter.writeData( + mockFile, + data.toByteArray(), + append = true + ) + + // Then + assertThat(result).isFalse() + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + EncryptedFileReaderWriter.APPEND_MODE_NOT_SUPPORTED_MESSAGE + ) + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockFileReaderWriterDelegate) + } + + // endregion + + // region FileReader#readData tests + + @Test + fun `M decrypt data W readData()`( + @StringForgery data: String + ) { + // Given + whenever( + mockFileReaderWriterDelegate.readData(mockFile) + ) doReturn encrypt(data.toByteArray()) + + // When + val result = testedReaderWriter.readData(mockFile) + + // Then + assertThat(result).isEqualTo(data.toByteArray()) + } + + // endregion + + // region writeData + readData + + @Test + fun `M return valid data W writeData() + readData()`( + @StringForgery data: String + ) { + // Given + var storage: ByteArray? = null + + whenever( + mockFileReaderWriterDelegate.writeData( + eq(mockFile), + any(), + eq(false) + ) + ) doAnswer { + storage = it.getArgument(1) + true + } + + whenever( + mockFileReaderWriterDelegate.readData(mockFile) + ) doAnswer { storage } + + // When + val writeResult = testedReaderWriter.writeData(mockFile, data.toByteArray(), false) + val readResult = testedReaderWriter.readData(mockFile) + + // Then + assertThat(writeResult).isTrue() + assertThat(readResult).isEqualTo(data.toByteArray()) + + verifyNoInteractions(mockInternalLogger) + } + + // endregion + + // region private + + // this is valid encryption-decryption pair, after the round we will get the original data + private fun encrypt(data: ByteArray): ByteArray { + return data.map { it.inv() }.toByteArray() + } + + private fun decrypt(data: ByteArray): ByteArray { + return data.map { it.inv() }.toByteArray() + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileExtTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileExtTest.kt new file mode 100644 index 0000000000..d766234c92 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileExtTest.kt @@ -0,0 +1,616 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.io.FileFilter +import java.nio.charset.Charset + +@Extensions( + ExtendWith( + MockitoExtension::class, + ForgeExtension::class + ) +) +@ForgeConfiguration(value = Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class FileExtTest { + + @TempDir + lateinit var tempDir: File + + @Mock + lateinit var mockFile: File + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @StringForgery + lateinit var fakeFileName: String + + lateinit var fakeFile: File + + @BeforeEach + fun `set up`() { + fakeFile = File(tempDir, fakeFileName) + } + + @Test + fun `M return result W canWriteSafe(mockInternalLogger)`( + @BoolForgery result: Boolean + ) { + // Given + whenever(mockFile.canWrite()) doReturn result + + // When + val canWrite = mockFile.canWriteSafe(mockInternalLogger) + + // Then + assertThat(canWrite).isEqualTo(result) + } + + @Test + fun `M catch exception W canWriteSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.canWrite()) doThrow SecurityException(message) + + // When + val canWrite = mockFile.canWriteSafe(mockInternalLogger) + + // Then + assertThat(canWrite).isFalse() + } + + @Test + fun `M return result W canReadSafe(mockInternalLogger)`( + @BoolForgery result: Boolean + ) { + // Given + whenever(mockFile.canRead()) doReturn result + + // When + val canRead = mockFile.canReadSafe(mockInternalLogger) + + // Then + assertThat(canRead).isEqualTo(result) + } + + @Test + fun `M catch exception W canReadSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.canRead()) doThrow SecurityException(message) + + // When + val canRead = mockFile.canReadSafe(mockInternalLogger) + + // Then + assertThat(canRead).isFalse() + } + + @Test + fun `M return result W deleteSafe(mockInternalLogger)`( + @BoolForgery result: Boolean + ) { + // Given + whenever(mockFile.delete()) doReturn result + + // When + val delete = mockFile.deleteSafe(mockInternalLogger) + + // Then + assertThat(delete).isEqualTo(result) + } + + @Test + fun `M catch exception W deleteSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.delete()) doThrow SecurityException(message) + + // When + val delete = mockFile.deleteSafe(mockInternalLogger) + + // Then + assertThat(delete).isFalse() + } + + @Test + fun `M return result W existsSafe(mockInternalLogger)`( + @BoolForgery result: Boolean + ) { + // Given + whenever(mockFile.exists()) doReturn result + + // When + val exists = mockFile.existsSafe(mockInternalLogger) + + // Then + assertThat(exists).isEqualTo(result) + } + + @Test + fun `M catch exception W existsSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.exists()) doThrow SecurityException(message) + + // When + val exists = mockFile.existsSafe(mockInternalLogger) + + // Then + assertThat(exists).isFalse() + } + + @Test + fun `M return result W isFileSafe(mockInternalLogger)`( + @BoolForgery result: Boolean + ) { + // Given + whenever(mockFile.isFile()) doReturn result + + // When + val isFile = mockFile.isFileSafe(mockInternalLogger) + + // Then + assertThat(isFile).isEqualTo(result) + } + + @Test + fun `M catch exception W isFileSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.isFile()) doThrow SecurityException(message) + + // When + val isFile = mockFile.isFileSafe(mockInternalLogger) + + // Then + assertThat(isFile).isFalse() + } + + @Test + fun `M return result W isDirectorySafe(mockInternalLogger)`( + @BoolForgery result: Boolean + ) { + // Given + whenever(mockFile.isDirectory()) doReturn result + + // When + val isDirectory = mockFile.isDirectorySafe(mockInternalLogger) + + // Then + assertThat(isDirectory).isEqualTo(result) + } + + @Test + fun `M catch exception W isDirectorySafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.isDirectory()) doThrow SecurityException(message) + + // When + val isDirectory = mockFile.isDirectorySafe(mockInternalLogger) + + // Then + assertThat(isDirectory).isFalse() + } + + @Test + fun `M return result W listFilesSafe(mockInternalLogger)`( + @Forgery result: List + ) { + // Given + whenever(mockFile.listFiles()) doReturn result.toTypedArray() + + // When + val listFiles = mockFile.listFilesSafe(mockInternalLogger) + + // Then + assertThat(listFiles).containsAll(result) + } + + @Test + fun `M catch exception W listFilesSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.listFiles()) doThrow SecurityException(message) + + // When + val listFiles = mockFile.listFilesSafe(mockInternalLogger) + + // Then + assertThat(listFiles).isNull() + } + + @Test + fun `M return result W listFilesSafe(filter)`( + @Forgery result: List + ) { + // Given + val filter: FileFilter = mock() + whenever(mockFile.listFiles(filter)) doReturn result.toTypedArray() + + // When + val listFiles = mockFile.listFilesSafe(filter, mockInternalLogger) + + // Then + assertThat(listFiles).containsAll(result) + } + + @Test + fun `M catch exception W listFilesSafe(filter) {SecurityException}`( + @StringForgery message: String + ) { + // Given + val filter: FileFilter = mock() + whenever(mockFile.listFiles(filter)) doThrow SecurityException(message) + + // When + val listFiles = mockFile.listFilesSafe(filter, mockInternalLogger) + + // Then + assertThat(listFiles).isNull() + } + + @Test + fun `M return result W lengthSafe(mockInternalLogger)`( + @LongForgery result: Long + ) { + // Given + whenever(mockFile.length()) doReturn result + + // When + val length = mockFile.lengthSafe(mockInternalLogger) + + // Then + assertThat(length).isEqualTo(result) + } + + @Test + fun `M catch exception W lengthSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.length()) doThrow SecurityException(message) + + // When + val length = mockFile.lengthSafe(mockInternalLogger) + + // Then + assertThat(length).isEqualTo(0L) + } + + @Test + fun `M return result W mkdirsSafe(mockInternalLogger)`( + @BoolForgery result: Boolean + ) { + // Given + whenever(mockFile.mkdirs()) doReturn result + + // When + val mkdirs = mockFile.mkdirsSafe(mockInternalLogger) + + // Then + assertThat(mkdirs).isEqualTo(result) + } + + @Test + fun `M catch exception W mkdirsSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.mkdirs()) doThrow SecurityException(message) + + // When + val mkdirs = mockFile.mkdirsSafe(mockInternalLogger) + + // Then + assertThat(mkdirs).isFalse() + } + + @Test + fun `M return result W renameToSafe(mockInternalLogger)`( + @BoolForgery result: Boolean, + @Forgery dest: File + ) { + // Given + whenever(mockFile.renameTo(dest)) doReturn result + + // When + val renameTo = mockFile.renameToSafe(dest, mockInternalLogger) + + // Then + assertThat(renameTo).isEqualTo(result) + } + + @Test + fun `M catch exception W renameToSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String, + @Forgery dest: File + ) { + // Given + whenever(mockFile.renameTo(dest)) doThrow SecurityException(message) + + // When + val renameTo = mockFile.renameToSafe(dest, mockInternalLogger) + + // Then + assertThat(renameTo).isFalse() + } + + @Test + fun `M return result W readTextSafe(mockInternalLogger)`( + @StringForgery result: String + ) { + // Given + fakeFile.writeText(result) + + // When + val readText = fakeFile.readTextSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readText).isEqualTo(result) + } + + @Test + fun `M return result W readTextSafe(mockInternalLogger) {custom charset}`( + @StringForgery result: String, + @Forgery charset: Charset + ) { + // Given + fakeFile.writeText(result, charset) + + // When + val readText = fakeFile.readTextSafe(charset, mockInternalLogger) + + // Then + assertThat(readText).isEqualTo(result) + } + + @Test + fun `M return null W readTextSafe(mockInternalLogger) {file doesn't exist}`() { + // Given + whenever(mockFile.exists()) doReturn false + + // When + val readText = mockFile.readTextSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readText).isNull() + } + + @Test + fun `M return null W readTextSafe(mockInternalLogger) {file can't be read}`() { + // Given + whenever(mockFile.exists()) doReturn true + whenever(mockFile.canRead()) doReturn false + + // When + val readText = mockFile.readTextSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readText).isNull() + } + + @Test + fun `M catch exception W readTextSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.name) doThrow SecurityException(message) + + // When + val readText = mockFile.readTextSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readText).isNull() + } + + @Test + fun `M catch exception W readTextSafe(mockInternalLogger) {FileNotFoundException}`() { + // Given + whenever(mockFile.exists()) doReturn true + whenever(mockFile.canRead()) doReturn true + whenever(mockFile.name) doReturn fakeFileName + + // When + val readText = mockFile.readTextSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readText).isNull() + } + + @Test + fun `M return result W readBytesSafe(mockInternalLogger)`( + @StringForgery result: String + ) { + // Given + fakeFile.writeText(result) + + // When + val readBytes = fakeFile.readBytesSafe(mockInternalLogger) + + // Then + assertThat(readBytes).isEqualTo(result.toByteArray()) + } + + @Test + fun `M return result W readBytesSafe(mockInternalLogger) {custom charset}`( + @StringForgery result: String, + @Forgery charset: Charset + ) { + // Given + fakeFile.writeText(result, charset) + + // When + val readBytes = fakeFile.readBytesSafe(mockInternalLogger) + + // Then + assertThat(readBytes).isEqualTo(result.toByteArray(charset)) + } + + @Test + fun `M return null W readBytesSafe(mockInternalLogger) {file doesn't exist}`() { + // Given + whenever(mockFile.exists()) doReturn false + + // When + val readBytes = mockFile.readBytesSafe(mockInternalLogger) + + // Then + assertThat(readBytes).isNull() + } + + @Test + fun `M return null W readBytesSafe(mockInternalLogger) {file can't be read}`() { + // Given + whenever(mockFile.exists()) doReturn true + whenever(mockFile.canRead()) doReturn false + + // When + val readBytes = mockFile.readBytesSafe(mockInternalLogger) + + // Then + assertThat(readBytes).isNull() + } + + @Test + fun `M catch exception W readBytesSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.name) doThrow SecurityException(message) + + // When + val readBytes = mockFile.readBytesSafe(mockInternalLogger) + + // Then + assertThat(readBytes).isNull() + } + + @Test + fun `M catch exception W readBytesSafe(mockInternalLogger) {FileNotFoundException}`() { + // Given + whenever(mockFile.exists()) doReturn true + whenever(mockFile.canRead()) doReturn true + whenever(mockFile.name) doReturn fakeFileName + + // When + val readBytes = mockFile.readBytesSafe(mockInternalLogger) + + // Then + assertThat(readBytes).isNull() + } + + @Test + fun `M return result W readLinesSafe(mockInternalLogger)`( + @StringForgery result: List + ) { + // Given + fakeFile.writeText(result.joinToString("\n")) + + // When + val readLines = fakeFile.readLinesSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readLines).isEqualTo(result) + } + + @Test + fun `M return result W readLinesSafe(mockInternalLogger) {custom charset}`( + @StringForgery result: List, + @Forgery charset: Charset + ) { + // Given + fakeFile.writeText(result.joinToString("\n"), charset) + + // When + val readLines = fakeFile.readLinesSafe(charset, mockInternalLogger) + + // Then + assertThat(readLines).isEqualTo(result) + } + + @Test + fun `M return null W readLinesSafe(mockInternalLogger) {file doesn't exist}`() { + // Given + whenever(mockFile.exists()) doReturn false + + // When + val readLines = mockFile.readLinesSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readLines).isNull() + } + + @Test + fun `M return null W readLinesSafe(mockInternalLogger) {file can't be read}`() { + // Given + whenever(mockFile.exists()) doReturn true + whenever(mockFile.canRead()) doReturn false + + // When + val readLines = mockFile.readLinesSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readLines).isNull() + } + + @Test + fun `M catch exception W readLinesSafe(mockInternalLogger) {SecurityException}`( + @StringForgery message: String + ) { + // Given + whenever(mockFile.name) doThrow SecurityException(message) + + // When + val readLines = mockFile.readLinesSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readLines).isNull() + } + + @Test + fun `M catch exception W readLinesSafe(mockInternalLogger) {FileNotFoundException}`() { + // When + val readLines = fakeFile.readLinesSafe(internalLogger = mockInternalLogger) + + // Then + assertThat(readLines).isNull() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileMoverTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileMoverTest.kt new file mode 100644 index 0000000000..d23fea1e30 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileMoverTest.kt @@ -0,0 +1,256 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.io.File +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class FileMoverTest { + + lateinit var testedFileMover: FileMover + + @StringForgery(regex = "([a-z]+)-([a-z]+)") + lateinit var fakeSrcDirName: String + + @StringForgery(regex = "([a-z]+)-([a-z]+)") + lateinit var fakeDstDirName: String + + @TempDir + lateinit var fakeRootDirectory: File + + @Mock + lateinit var mockInternalLogger: InternalLogger + + private lateinit var fakeSrcDir: File + private lateinit var fakeDstDir: File + + @BeforeEach + fun setUp() { + fakeSrcDir = File(fakeRootDirectory, fakeSrcDirName) + fakeDstDir = File(fakeRootDirectory, fakeDstDirName) + testedFileMover = FileMover(mockInternalLogger) + } + + // region delete + + @Test + fun `M delete file W delete()`( + @StringForgery fileName: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.createNewFile() + + // When + val result = testedFileMover.delete(file) + + // Then + assertThat(result).isTrue() + assertThat(file).doesNotExist() + } + + @Test + fun `M delete folder recursively W delete()`( + @StringForgery dirName: String, + @StringForgery fileName: String, + @IntForgery(1, 64) fileCount: Int + ) { + // Given + val dir = File(fakeRootDirectory, dirName) + dir.mkdir() + for (i in 0 until fileCount) { + File(dir, "$fileName$i").createNewFile() + } + + // When + val result = testedFileMover.delete(dir) + + // Then + assertThat(result).isTrue() + assertThat(dir).doesNotExist() + } + + @Test + fun `M delete folder recursively W delete() {nested dirs}`( + @StringForgery dirName: String, + @StringForgery fileName: String, + @IntForgery(1, 10) fileCount: Int + ) { + // Given + val dir = File(fakeRootDirectory, dirName) + dir.mkdir() + var parent = dir + for (i in 1 until fileCount) { + parent = File(parent, "$dirName$i") + parent.mkdir() + } + val file = File(parent, fileName) + file.createNewFile() + + // When + val result = testedFileMover.delete(dir) + + // Then + assertThat(result).isTrue() + assertThat(dir).doesNotExist() + assertThat(file).doesNotExist() + } + + // endregion + + // region moveFiles + + @Test + fun `M return true and warn W moveFiles() {source dir does not exist}`() { + // Given + assumeFalse(fakeSrcDir.exists()) + fakeDstDir.mkdirs() + + // When + val result = testedFileMover.moveFiles(fakeSrcDir, fakeDstDir) + + // Then + assertThat(result).isTrue() + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.MAINTAINER, + FileMover.INFO_MOVE_NO_SRC.format(Locale.US, fakeSrcDir.path) + ) + } + + @Test + fun `M return false and warn W moveFiles() {source dir is not a dir}`() { + // Given + fakeSrcDir.createNewFile() + fakeDstDir.mkdirs() + + // When + val result = testedFileMover.moveFiles(fakeSrcDir, fakeDstDir) + + // Then + assertThat(result).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + FileMover.ERROR_MOVE_NOT_DIR.format(Locale.US, fakeSrcDir.path) + ) + } + + @Test + fun `M return false and warn W moveFiles() {dest dir is not a dir}`() { + // Given + fakeSrcDir.mkdirs() + fakeDstDir.createNewFile() + + // When + val result = testedFileMover.moveFiles(fakeSrcDir, fakeDstDir) + + // Then + assertThat(result).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + FileMover.ERROR_MOVE_NOT_DIR.format(Locale.US, fakeDstDir.path) + ) + } + + @Test + fun `M move all files and return true W moveFiles()`( + @StringForgery fileNames: List + ) { + // Given + fakeSrcDir.mkdirs() + fakeDstDir.mkdirs() + fileNames.forEach { name -> + File(fakeSrcDir, name).writeText(name.reversed()) + } + val expectedFiles = fileNames.map { name -> + File(fakeDstDir, name) + } + + // When + val result = testedFileMover.moveFiles(fakeSrcDir, fakeDstDir) + + // Then + assertThat(result).isTrue() + assertThat(fakeSrcDir.listFiles()).isEmpty() + expectedFiles.forEach { + assertThat(it).exists() + .hasContent(it.name.reversed()) + } + } + + @Test + fun `M do nothing and return true W moveFiles() {source dir is empty}`() { + // Given + fakeSrcDir.mkdirs() + fakeDstDir.mkdirs() + + // When + val result = testedFileMover.moveFiles(fakeSrcDir, fakeDstDir) + + // Then + assertThat(result).isTrue() + assertThat(fakeSrcDir.listFiles()).isEmpty() + assertThat(fakeDstDir.listFiles()).isEmpty() + } + + @Test + fun `M create dest, move all files and return true W moveFiles() {dest dir does not exist}`( + @StringForgery fileNamesInput: List + ) { + // Given + fakeSrcDir.mkdirs() + assumeFalse(fakeDstDir.exists()) + + // in case of file system is not case-sensitive, we need to drop all duplicates + val fileNames = fileNamesInput.distinctBy { it.lowercase(Locale.US) } + fileNames.forEach { name -> + File(fakeSrcDir, name).writeText(name.reversed()) + } + val expectedFiles = fileNames.map { name -> + File(fakeDstDir, name) + } + + // When + val result = testedFileMover.moveFiles(fakeSrcDir, fakeDstDir) + + // Then + assertThat(result).isTrue() + assertThat(fakeSrcDir.listFiles()).isEmpty() + expectedFiles.forEach { + assertThat(it).exists() + .hasContent(it.name.reversed()) + } + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileReaderWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileReaderWriterTest.kt new file mode 100644 index 0000000000..3f13475b67 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/FileReaderWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import com.datadog.android.api.InternalLogger +import com.datadog.android.security.Encryption +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class FileReaderWriterTest { + + @Mock + lateinit var mockLogger: InternalLogger + + @Test + fun `M create FileReaderWriter W create() { without encryption }`() { + // When + val readerWriter = FileReaderWriter.create(mockLogger, null) + // Then + assertThat(readerWriter) + .isInstanceOf(PlainFileReaderWriter::class.java) + } + + @Test + fun `M create FileReaderWriter W create() { with encryption }`() { + // When + val mockEncryption = mock() + val readerWriter = FileReaderWriter.create( + mockLogger, + mockEncryption + ) + + // Then + assertThat(readerWriter) + .isInstanceOf(EncryptedFileReaderWriter::class.java) + + (readerWriter as EncryptedFileReaderWriter).let { + assertThat(it.delegate).isInstanceOf(PlainFileReaderWriter::class.java) + assertThat(it.encryption).isEqualTo(mockEncryption) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/PlainFileReaderWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/PlainFileReaderWriterTest.kt new file mode 100644 index 0000000000..c9be924967 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/PlainFileReaderWriterTest.kt @@ -0,0 +1,349 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.io.File +import java.io.IOException +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class PlainFileReaderWriterTest { + + private lateinit var testedFileReaderWriter: PlainFileReaderWriter + + @StringForgery(regex = "([a-z]+)-([a-z]+)") + lateinit var fakeSrcDirName: String + + @StringForgery(regex = "([a-z]+)-([a-z]+)") + lateinit var fakeDstDirName: String + + @TempDir + lateinit var fakeRootDirectory: File + + @Mock + lateinit var mockInternalLogger: InternalLogger + + private lateinit var fakeSrcDir: File + private lateinit var fakeDstDir: File + + @BeforeEach + fun `set up`() { + fakeSrcDir = File(fakeRootDirectory, fakeSrcDirName) + fakeDstDir = File(fakeRootDirectory, fakeDstDirName) + testedFileReaderWriter = PlainFileReaderWriter(mockInternalLogger) + } + + // region writeData + + @Test + fun `M write data in empty file W writeData() {append=false}`( + @StringForgery fileName: String, + @StringForgery content: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.createNewFile() + val contentBytes = content.toByteArray() + + // When + val result = testedFileReaderWriter.writeData( + file, + contentBytes, + append = false + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists().hasBinaryContent(contentBytes) + } + + @Test + fun `M write data in empty file W writeData() {append=true}`( + @StringForgery fileName: String, + @StringForgery content: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.createNewFile() + val contentBytes = content.toByteArray() + + // When + val result = testedFileReaderWriter.writeData( + file, + contentBytes, + append = false + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists().hasBinaryContent(contentBytes) + } + + @Test + fun `M overwrite data in non empty file W writeData() {append=false}`( + @StringForgery fileName: String, + @StringForgery previousContent: String, + @StringForgery content: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.writeText(previousContent) + val contentBytes = content.toByteArray() + + // When + val result = testedFileReaderWriter.writeData( + file, + contentBytes, + append = false + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists().hasBinaryContent(contentBytes) + } + + @Test + fun `M append data in non empty file W writeData() {append=true}`( + @StringForgery fileName: String, + @StringForgery previousContent: String, + @StringForgery content: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + val previousData = previousContent.toByteArray() + file.writeBytes(previousData) + val contentBytes = content.toByteArray() + + // When + val result = testedFileReaderWriter.writeData( + file, + contentBytes, + append = true + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists() + .hasBinaryContent( + previousData + contentBytes + ) + } + + @Test + fun `M return false and warn W writeData() {parent dir does not exist}`( + @StringForgery fileName: String, + @StringForgery content: String, + @BoolForgery append: Boolean + ) { + // Given + assumeFalse(fakeSrcDir.exists()) + val file = File(fakeSrcDir, fileName) + + // When + val result = testedFileReaderWriter.writeData( + file, + content.toByteArray(), + append = append + ) + + // Then + assertThat(result).isFalse() + assertThat(file).doesNotExist() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + PlainFileReaderWriter.ERROR_WRITE.format(Locale.US, file.path), + IOException::class.java + ) + } + + @Test + fun `M return false and warn W writeData() {file is not file}`( + @StringForgery fileName: String, + @StringForgery content: String, + @BoolForgery append: Boolean + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.mkdirs() + + // When + val result = testedFileReaderWriter.writeData( + file, + content.toByteArray(), + append = append + ) + + // Then + assertThat(result).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + PlainFileReaderWriter.ERROR_WRITE.format(Locale.US, file.path), + IOException::class.java + ) + } + + // endregion + + // region readData + + @Test + fun `M return empty array and warn W readData() {file does not exist}`( + @StringForgery fileName: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + assumeFalse(file.exists()) + + // When + val result = testedFileReaderWriter.readData(file) + + // Then + assertThat(result).isEmpty() + assertThat(file).doesNotExist() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + PlainFileReaderWriter.ERROR_READ.format(Locale.US, file.path), + null + ) + } + + @Test + fun `M return empty array and warn W readData() {file is not file}`( + @StringForgery fileName: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + assumeFalse(file.exists()) + + // When + val result = testedFileReaderWriter.readData(file) + + // Then + assertThat(result).isEmpty() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + PlainFileReaderWriter.ERROR_READ.format(Locale.US, file.path), + null + ) + } + + @Test + fun `M return file content W readData() { single event }`( + @StringForgery fileName: String, + @StringForgery event: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + val eventBytes = event.toByteArray() + file.writeBytes(eventBytes) + + // When + val result = testedFileReaderWriter.readData(file) + + // Then + assertThat(result).isEqualTo(eventBytes) + } + + @Test + fun `M return file content W readData() { multiple events }`( + @StringForgery fileName: String, + forge: Forge + ) { + // Given + val file = File(fakeRootDirectory, fileName) + val events = forge.aList { + aString().toByteArray() + } + val data = events.reduce { acc, bytes -> acc + bytes } + file.writeBytes(data) + + // When + val result = testedFileReaderWriter.readData(file) + + // Then + assertThat(result).isEqualTo(data) + } + + // endregion + + // region writeData + readData + + @Test + fun `M return file content W writeData + readData() { append = false }`( + @StringForgery fileName: String, + @StringForgery content: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + + // When + val writeResult = testedFileReaderWriter.writeData(file, content.toByteArray(), false) + val readResult = testedFileReaderWriter.readData(file) + + // Then + assertThat(writeResult).isTrue() + assertThat(readResult).isEqualTo(content.toByteArray()) + } + + @Test + fun `M return file content W writeData + readData() { append = true }`( + @StringForgery fileName: String, + forge: Forge + ) { + // Given + val file = File(fakeRootDirectory, fileName) + + val data = forge.aList { + aString().toByteArray() + } + + // When + var writeResult = true + data.forEach { + writeResult = writeResult && testedFileReaderWriter.writeData( + file, + it, + true + ) + } + val readResult = testedFileReaderWriter.readData(file) + + // Then + assertThat(writeResult).isTrue() + assertThat(readResult).isEqualTo(data.reduce { acc, bytes -> acc + bytes }) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileMigratorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileMigratorTest.kt new file mode 100644 index 0000000000..a2697700e7 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileMigratorTest.kt @@ -0,0 +1,208 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ConsentAwareFileMigratorTest { + + lateinit var testedMigrator: DataMigrator + + @Mock + lateinit var mockPreviousOrchestrator: FileOrchestrator + + @Mock + lateinit var mockNewOrchestrator: FileOrchestrator + + @Mock + lateinit var mockFileMover: FileMover + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedMigrator = ConsentAwareFileMigrator( + mockFileMover, + mockInternalLogger + ) + } + + @RepeatedTest(8) + fun `M wipe pending data W migrateData() {null to any}`( + @Forgery consent: TrackingConsent, + @Forgery pendingDir: File + ) { + // Given + whenever(mockPreviousOrchestrator.getRootDir()) doReturn pendingDir + whenever(mockFileMover.delete(pendingDir)) doReturn true + + // When + testedMigrator.migrateData( + null, + mockPreviousOrchestrator, + consent, + mockNewOrchestrator + ) + + // Then + verify(mockFileMover).delete(pendingDir) + } + + @Test + fun `M wipe pending data W migrateData() {PENDING to NOT_GRANTED}`( + @Forgery pendingDir: File + ) { + // Given + whenever(mockPreviousOrchestrator.getRootDir()) doReturn pendingDir + whenever(mockFileMover.delete(pendingDir)) doReturn true + + // When + testedMigrator.migrateData( + TrackingConsent.PENDING, + mockPreviousOrchestrator, + TrackingConsent.NOT_GRANTED, + mockNewOrchestrator + ) + + // Then + verify(mockFileMover).delete(pendingDir) + } + + @Test + fun `M wipe pending data W migrateData() {GRANTED to PENDING}`( + @Forgery pendingDir: File + ) { + // Given + whenever(mockNewOrchestrator.getRootDir()) doReturn pendingDir + whenever(mockFileMover.delete(pendingDir)) doReturn true + + // When + testedMigrator.migrateData( + TrackingConsent.GRANTED, + mockPreviousOrchestrator, + TrackingConsent.PENDING, + mockNewOrchestrator + ) + + // Then + verify(mockFileMover).delete(pendingDir) + } + + @Test + fun `M wipe pending data W migrateData() {NOT_GRANTED to PENDING}`( + @Forgery pendingDir: File + ) { + // Given + whenever(mockNewOrchestrator.getRootDir()) doReturn pendingDir + whenever(mockFileMover.delete(pendingDir)) doReturn true + + // When + testedMigrator.migrateData( + TrackingConsent.NOT_GRANTED, + mockPreviousOrchestrator, + TrackingConsent.PENDING, + mockNewOrchestrator + ) + + // Then + verify(mockFileMover).delete(pendingDir) + } + + @Test + fun `M move pending data W migrateData() {PENDING to GRANTED}`( + @Forgery pendingDir: File, + @Forgery grantedDir: File + ) { + // Given + whenever(mockPreviousOrchestrator.getRootDir()) doReturn pendingDir + whenever(mockNewOrchestrator.getRootDir()) doReturn grantedDir + whenever(mockFileMover.moveFiles(pendingDir, grantedDir)) doReturn true + + // When + testedMigrator.migrateData( + TrackingConsent.PENDING, + mockPreviousOrchestrator, + TrackingConsent.GRANTED, + mockNewOrchestrator + ) + + // Then + verify(mockFileMover).moveFiles(pendingDir, grantedDir) + } + + @RepeatedTest(8) + fun `M do nothing W migrateData() {x to x}`( + @Forgery consent: TrackingConsent + ) { + // When + testedMigrator.migrateData( + consent, + mockPreviousOrchestrator, + consent, + mockNewOrchestrator + ) + + // Then + verifyNoInteractions(mockFileMover) + } + + @Test + fun `M do nothing W migrateData() {GRANTED to NOT_GRANTED}`() { + // When + testedMigrator.migrateData( + TrackingConsent.GRANTED, + mockPreviousOrchestrator, + TrackingConsent.NOT_GRANTED, + mockNewOrchestrator + ) + + // Then + verifyNoInteractions(mockFileMover) + } + + @Test + fun `M do nothing W migrateData() {NOT_GRANTED to GRANTED}`() { + // When + testedMigrator.migrateData( + TrackingConsent.NOT_GRANTED, + mockPreviousOrchestrator, + TrackingConsent.GRANTED, + mockNewOrchestrator + ) + + // Then + verifyNoInteractions(mockFileMover) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileOrchestratorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileOrchestratorTest.kt new file mode 100644 index 0000000000..6d076300b7 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ConsentAwareFileOrchestratorTest.kt @@ -0,0 +1,868 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.concurrent.ExecutorService +import java.util.concurrent.RejectedExecutionException + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ConsentAwareFileOrchestratorTest { + + lateinit var testedOrchestrator: ConsentAwareFileOrchestrator + + @Mock + lateinit var mockConsentProvider: ConsentProvider + + @Mock + lateinit var mockPendingOrchestrator: FileOrchestrator + + @Mock + lateinit var mockGrantedOrchestrator: FileOrchestrator + + @Mock + lateinit var mockExecutorService: ExecutorService + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockDataMigrator: DataMigrator + + @BeforeEach + fun `set up`() { + instantiateTestedOrchestrator(TrackingConsent.PENDING) + runPendingRunnable() + reset(mockDataMigrator, mockConsentProvider, mockExecutorService) + } + + // region init + + @Test + fun `M registers as listener W init()`( + @Forgery consent: TrackingConsent + ) { + // When + instantiateTestedOrchestrator(consent) + + // Then + verify(mockConsentProvider).registerCallback(testedOrchestrator) + } + + @Test + fun `M migrate data W init() {GRANTED}`() { + // When + instantiateTestedOrchestrator(TrackingConsent.GRANTED) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + null, + mockPendingOrchestrator, + TrackingConsent.GRANTED, + mockGrantedOrchestrator + ) + } + } + + @Test + fun `M migrate data W init() {PENDING}`() { + // When + instantiateTestedOrchestrator(TrackingConsent.PENDING) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + null, + mockPendingOrchestrator, + TrackingConsent.PENDING, + mockPendingOrchestrator + ) + } + } + + @Test + fun `M migrate data W init() {NOT_GRANTED}`() { + // When + instantiateTestedOrchestrator(TrackingConsent.NOT_GRANTED) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + null, + mockPendingOrchestrator, + TrackingConsent.NOT_GRANTED, + ConsentAwareFileOrchestrator.NO_OP_ORCHESTRATOR + ) + } + } + + // endregion + + // region getWritableFile + + @Test + fun `M return pending writable file W getWritableFile() {consent=PENDING}`( + @Forgery file: File + ) { + // Given + whenever(mockPendingOrchestrator.getWritableFile()) doReturn file + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockGrantedOrchestrator) + } + + @Test + fun `M return pending writable file W getWritableFile() {consent=GRANTED then PENDING}`( + @Forgery file: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.GRANTED) + runPendingRunnable() + whenever(mockPendingOrchestrator.getWritableFile()) doReturn file + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.PENDING) + runPendingRunnable() + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockGrantedOrchestrator) + } + + @Test + fun `M return pending writable file W getWritableFile() {consent=NOT_GRANTED then PENDING}`( + @Forgery file: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.NOT_GRANTED) + runPendingRunnable() + whenever(mockPendingOrchestrator.getWritableFile()) doReturn file + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.PENDING) + runPendingRunnable() + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockGrantedOrchestrator) + } + + @Test + fun `M return granted writable file W getWritableFile() {consent=GRANTED}`( + @Forgery file: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.GRANTED) + runPendingRunnable() + whenever(mockGrantedOrchestrator.getWritableFile()) doReturn file + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockPendingOrchestrator) + } + + @Test + fun `M return granted writable file W getWritableFile() {consent=NOT_GRANTED then GRANTED}`( + @Forgery file: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.NOT_GRANTED) + runPendingRunnable() + whenever(mockGrantedOrchestrator.getWritableFile()) doReturn file + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockPendingOrchestrator) + } + + @Test + fun `M return granted writable file W getWritableFile() {consent=PENDING then GRANTED}`( + @Forgery file: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.PENDING) + runPendingRunnable() + whenever(mockGrantedOrchestrator.getWritableFile()) doReturn file + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockPendingOrchestrator) + } + + @Test + fun `M return null file W getWritableFile() {consent=NOT_GRANTED}`() { + // Given + instantiateTestedOrchestrator(TrackingConsent.NOT_GRANTED) + runPendingRunnable() + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + @Test + fun `M return null file W getWritableFile() {consent=GRANTED then NOT_GRANTED}`() { + // Given + instantiateTestedOrchestrator(TrackingConsent.GRANTED) + runPendingRunnable() + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.NOT_GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + @Test + fun `M return null file W getWritableFile() {consent=PENDING then NOT_GRANTED}`() { + // Given + instantiateTestedOrchestrator(TrackingConsent.PENDING) + runPendingRunnable() + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.NOT_GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + // endregion + + // region getReadableFile + + @Test + fun `M return granted file W getReadableFile() {initial consent}`( + @Forgery consent: TrackingConsent, + @Forgery file: File + ) { + // Given + instantiateTestedOrchestrator(consent) + whenever(mockGrantedOrchestrator.getReadableFile(any())) doReturn file + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockPendingOrchestrator) + } + + @Test + fun `M return granted file W getReadableFile() {updated consent}`( + @Forgery initialConsent: TrackingConsent, + @Forgery updatedConsent: TrackingConsent, + @Forgery file: File + ) { + // Given + instantiateTestedOrchestrator(initialConsent) + whenever(mockGrantedOrchestrator.getReadableFile(any())) doReturn file + + // When + testedOrchestrator.onConsentUpdated(initialConsent, updatedConsent) + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isSameAs(file) + verifyNoInteractions(mockPendingOrchestrator) + } + + // endregion + + // region getAllFiles + + @Test + fun `M return all files W getAllFiles() {initial consent}`( + @Forgery consent: TrackingConsent, + forge: Forge + ) { + // Given + instantiateTestedOrchestrator(consent) + val pendingFiles = forge.aList { getForgery() } + val grantedFiles = forge.aList { getForgery() } + whenever(mockPendingOrchestrator.getAllFiles()) doReturn pendingFiles + whenever(mockGrantedOrchestrator.getAllFiles()) doReturn grantedFiles + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result) + .containsAll(pendingFiles) + .containsAll(grantedFiles) + } + + @Test + fun `M return all files W getAllFiles() {updated consent}`( + @Forgery initialConsent: TrackingConsent, + @Forgery updatedConsent: TrackingConsent, + forge: Forge + ) { + // Given + instantiateTestedOrchestrator(initialConsent) + val pendingFiles = forge.aList { getForgery() } + val grantedFiles = forge.aList { getForgery() } + whenever(mockPendingOrchestrator.getAllFiles()) doReturn pendingFiles + whenever(mockGrantedOrchestrator.getAllFiles()) doReturn grantedFiles + + // When + testedOrchestrator.onConsentUpdated(initialConsent, updatedConsent) + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result) + .containsAll(pendingFiles) + .containsAll(grantedFiles) + } + + // endregion + + // region getGrantedFiles + + @Test + fun `M return granted files W getFlushableFiles()`( + @Forgery consent: TrackingConsent, + forge: Forge + ) { + // Given + instantiateTestedOrchestrator(consent) + val pendingFiles = forge.aList { getForgery() } + val grantedFiles = forge.aList { getForgery() } + whenever(mockPendingOrchestrator.getFlushableFiles()) doReturn pendingFiles + whenever(mockGrantedOrchestrator.getFlushableFiles()) doReturn grantedFiles + + // When + val result = testedOrchestrator.getFlushableFiles() + + // Then + assertThat(result) + .containsExactlyElementsOf(grantedFiles) + } + + // endregion + + // region getRootDir + + @RepeatedTest(8) + fun `M return null W getRootDir() {initial consent}`( + @Forgery consent: TrackingConsent + ) { + // Given + instantiateTestedOrchestrator(consent) + + // When + val result = testedOrchestrator.getRootDir() + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + @RepeatedTest(16) + fun `M return null W getRootDir() {updated consent}`( + @Forgery initialConsent: TrackingConsent, + @Forgery updatedConsent: TrackingConsent + ) { + // Given + instantiateTestedOrchestrator(initialConsent) + + // When + testedOrchestrator.onConsentUpdated(initialConsent, updatedConsent) + val result = testedOrchestrator.getRootDir() + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + // endregion + + // region getRootDirName + + @Test + fun `M return null W getRootDirName() {initial consent}`( + @Forgery consent: TrackingConsent + ) { + // Given + instantiateTestedOrchestrator(consent) + + // When + val result = testedOrchestrator.getRootDirName() + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + @Test + fun `M return null W getRootDirName() {updated consent}`( + @Forgery initialConsent: TrackingConsent, + @Forgery updatedConsent: TrackingConsent + ) { + // Given + instantiateTestedOrchestrator(initialConsent) + + // When + testedOrchestrator.onConsentUpdated(initialConsent, updatedConsent) + val result = testedOrchestrator.getRootDirName() + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + // endregion + + // region getMetadataFile + + @Test + fun `M return pending meta file W getMetadataFile() {consent=PENDING}`( + @Forgery fakeFile: File, + @Forgery metaFile: File + ) { + // Given + whenever(mockPendingOrchestrator.getMetadataFile(fakeFile)) doReturn metaFile + + // When + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isSameAs(metaFile) + verifyNoInteractions(mockGrantedOrchestrator) + } + + @Test + fun `M return pending meta file W getMetadataFile() {consent=GRANTED then PENDING}`( + @Forgery fakeFile: File, + @Forgery metaFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.GRANTED) + runPendingRunnable() + whenever(mockPendingOrchestrator.getMetadataFile(fakeFile)) doReturn metaFile + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.PENDING) + runPendingRunnable() + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isSameAs(metaFile) + verifyNoInteractions(mockGrantedOrchestrator) + } + + @Test + fun `M return pending meta file W getMetadataFile() {consent=NOT_GRANTED then PENDING}`( + @Forgery fakeFile: File, + @Forgery metaFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.NOT_GRANTED) + runPendingRunnable() + whenever(mockPendingOrchestrator.getMetadataFile(fakeFile)) doReturn metaFile + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.PENDING) + runPendingRunnable() + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isSameAs(metaFile) + verifyNoInteractions(mockGrantedOrchestrator) + } + + @Test + fun `M return granted meta file W getMetadataFile() {consent=GRANTED}`( + @Forgery fakeFile: File, + @Forgery metaFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.GRANTED) + runPendingRunnable() + whenever(mockGrantedOrchestrator.getMetadataFile(fakeFile)) doReturn metaFile + + // When + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isSameAs(metaFile) + verifyNoInteractions(mockPendingOrchestrator) + } + + @Test + fun `M return granted meta file W getMetadataFile() {consent=NOT_GRANTED then GRANTED}`( + @Forgery fakeFile: File, + @Forgery metaFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.NOT_GRANTED) + runPendingRunnable() + whenever(mockGrantedOrchestrator.getMetadataFile(fakeFile)) doReturn metaFile + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isSameAs(metaFile) + verifyNoInteractions(mockPendingOrchestrator) + } + + @Test + fun `M return granted meta file W getMetadataFile() {consent=PENDING then GRANTED}`( + @Forgery fakeFile: File, + @Forgery metaFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.PENDING) + runPendingRunnable() + whenever(mockGrantedOrchestrator.getMetadataFile(fakeFile)) doReturn metaFile + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isSameAs(metaFile) + verifyNoInteractions(mockPendingOrchestrator) + } + + @Test + fun `M return null file W getMetadataFile() {consent=NOT_GRANTED}`( + @Forgery fakeFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.NOT_GRANTED) + runPendingRunnable() + + // When + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + @Test + fun `M return null file W getMetadataFile() {consent=GRANTED then NOT_GRANTED}`( + @Forgery fakeFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.GRANTED) + runPendingRunnable() + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.NOT_GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + @Test + fun `M return null file W getMetadataFile() {consent=PENDING then NOT_GRANTED}`( + @Forgery fakeFile: File + ) { + // Given + instantiateTestedOrchestrator(TrackingConsent.PENDING) + runPendingRunnable() + + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.NOT_GRANTED) + runPendingRunnable() + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isNull() + verifyNoInteractions(mockPendingOrchestrator, mockGrantedOrchestrator) + } + + // endregion + + // region onConsentUpdated + + @Test + fun `M migrate data W onConsentUpdated() {GRANTED to GRANTED}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.GRANTED) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.GRANTED, + mockGrantedOrchestrator, + TrackingConsent.GRANTED, + mockGrantedOrchestrator + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {GRANTED to PENDING}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.PENDING) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.GRANTED, + mockGrantedOrchestrator, + TrackingConsent.PENDING, + mockPendingOrchestrator + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {GRANTED to NOT_GRANTED}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.GRANTED, TrackingConsent.NOT_GRANTED) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.GRANTED, + mockGrantedOrchestrator, + TrackingConsent.NOT_GRANTED, + ConsentAwareFileOrchestrator.NO_OP_ORCHESTRATOR + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {PENDING to GRANTED}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.GRANTED) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.PENDING, + mockPendingOrchestrator, + TrackingConsent.GRANTED, + mockGrantedOrchestrator + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {PENDING to PENDING}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.PENDING) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.PENDING, + mockPendingOrchestrator, + TrackingConsent.PENDING, + mockPendingOrchestrator + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {PENDING to NOT_GRANTED}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.PENDING, TrackingConsent.NOT_GRANTED) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.PENDING, + mockPendingOrchestrator, + TrackingConsent.NOT_GRANTED, + ConsentAwareFileOrchestrator.NO_OP_ORCHESTRATOR + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {NOT_GRANTED to GRANTED}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.GRANTED) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.NOT_GRANTED, + ConsentAwareFileOrchestrator.NO_OP_ORCHESTRATOR, + TrackingConsent.GRANTED, + mockGrantedOrchestrator + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {NOT_GRANTED to PENDING}`() { + // When + testedOrchestrator.onConsentUpdated(TrackingConsent.NOT_GRANTED, TrackingConsent.PENDING) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.NOT_GRANTED, + ConsentAwareFileOrchestrator.NO_OP_ORCHESTRATOR, + TrackingConsent.PENDING, + mockPendingOrchestrator + ) + } + } + + @Test + fun `M migrate data W onConsentUpdated() {NOT_GRANTED to NOT_GRANTED}`() { + // When + testedOrchestrator.onConsentUpdated( + TrackingConsent.NOT_GRANTED, + TrackingConsent.NOT_GRANTED + ) + + // Then + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDataMigrator).migrateData( + TrackingConsent.NOT_GRANTED, + ConsentAwareFileOrchestrator.NO_OP_ORCHESTRATOR, + TrackingConsent.NOT_GRANTED, + ConsentAwareFileOrchestrator.NO_OP_ORCHESTRATOR + ) + } + } + + @Test + fun `M warn W onConsentUpdated() {submission rejected}`( + @Forgery previousConsent: TrackingConsent, + @Forgery newConsent: TrackingConsent, + @StringForgery errorMessage: String + ) { + // Given + val exception = RejectedExecutionException(errorMessage) + whenever(mockExecutorService.execute(any())) doThrow exception + + // When + testedOrchestrator.onConsentUpdated(previousConsent, newConsent) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule Data migration task on the executor", + exception + ) + } + + // endregion + + private fun instantiateTestedOrchestrator(consent: TrackingConsent) { + whenever(mockConsentProvider.getConsent()) doReturn consent + testedOrchestrator = ConsentAwareFileOrchestrator( + mockConsentProvider, + mockPendingOrchestrator, + mockGrantedOrchestrator, + mockDataMigrator, + mockExecutorService, + mockInternalLogger + ) + } + + private fun runPendingRunnable() { + argumentCaptor { + verify(mockExecutorService, atLeast(0)).execute(capture()) + allValues.forEach { it.run() } + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestratorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestratorTest.kt new file mode 100644 index 0000000000..27fcecabbf --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestratorTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.metrics.MetricsDispatcher +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.batch.BatchFileOrchestrator +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.concurrent.ExecutorService + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class FeatureFileOrchestratorTest { + + @Mock + lateinit var mockConsentProvider: ConsentProvider + + @Mock + lateinit var mockExecutorService: ExecutorService + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @StringForgery + lateinit var fakeFeatureName: String + + @Forgery + lateinit var fakeConsent: TrackingConsent + + @TempDir + lateinit var fakeStorageDir: File + + @Forgery + lateinit var fakeFilePersistenceConfig: FilePersistenceConfig + + @Mock + lateinit var mockMetricsDispatcher: MetricsDispatcher + + @BeforeEach + fun `set up`() { + whenever(mockConsentProvider.getConsent()) doReturn fakeConsent + } + + @Test + fun `M initialise pending orchestrator in cache dir W init()`() { + // Given + + // When + val orchestrator = FeatureFileOrchestrator( + mockConsentProvider, + fakeStorageDir, + fakeFeatureName, + mockExecutorService, + fakeFilePersistenceConfig, + mockInternalLogger, + mockMetricsDispatcher + ) + + // Then + assertThat(orchestrator.pendingOrchestrator) + .isInstanceOf(BatchFileOrchestrator::class.java) + assertThat(orchestrator.pendingOrchestrator.getRootDir()) + .isEqualTo(File(fakeStorageDir, "$fakeFeatureName-pending-v2")) + } + + @Test + fun `M initialise granted orchestrator in cache dir W init()`() { + // Given + + // When + val orchestrator = FeatureFileOrchestrator( + mockConsentProvider, + fakeStorageDir, + fakeFeatureName, + mockExecutorService, + fakeFilePersistenceConfig, + mockInternalLogger, + mockMetricsDispatcher + ) + + // Then + assertThat(orchestrator.grantedOrchestrator) + .isInstanceOf(BatchFileOrchestrator::class.java) + assertThat(orchestrator.grantedOrchestrator.getRootDir()) + .isEqualTo(File(fakeStorageDir, "$fakeFeatureName-v2")) + } + + @Test + fun `M use a consent aware migrator W init()`() { + // Given + + // When + val orchestrator = FeatureFileOrchestrator( + mockConsentProvider, + fakeStorageDir, + fakeFeatureName, + mockExecutorService, + fakeFilePersistenceConfig, + mockInternalLogger, + mockMetricsDispatcher + ) + + // Then + assertThat(orchestrator.dataMigrator) + .isInstanceOf(ConsentAwareFileMigrator::class.java) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperationTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperationTest.kt new file mode 100644 index 0000000000..4f93549804 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperationTest.kt @@ -0,0 +1,178 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import kotlin.system.measureTimeMillis + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class MoveDataMigrationOperationTest { + lateinit var testedOperation: DataMigrationOperation + + @TempDir + lateinit var fakeFromDirectory: File + + @TempDir + lateinit var fakeToDirectory: File + + @Mock + lateinit var mockFileMover: FileMover + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedOperation = MoveDataMigrationOperation( + fakeFromDirectory, + fakeToDirectory, + mockFileMover, + mockInternalLogger + ) + } + + @Test + fun `M warn W run() {source dir is null}`() { + // Given + testedOperation = MoveDataMigrationOperation( + null, + fakeToDirectory, + mockFileMover, + mockInternalLogger + ) + + // When + testedOperation.run() + + // Then + verifyNoInteractions(mockFileMover) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + MoveDataMigrationOperation.WARN_NULL_SOURCE_DIR + ) + } + + @Test + fun `M warn W run() {dest dir is null}`() { + // Given + testedOperation = MoveDataMigrationOperation( + fakeFromDirectory, + null, + mockFileMover, + mockInternalLogger + ) + whenever(mockFileMover.delete(fakeFromDirectory)) doReturn true + + // When + testedOperation.run() + + // Then + verifyNoInteractions(mockFileMover) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + MoveDataMigrationOperation.WARN_NULL_DEST_DIR + ) + } + + @Test + fun `M move data W run()`() { + // Given + whenever(mockFileMover.moveFiles(fakeFromDirectory, fakeToDirectory)) doReturn true + + // When + testedOperation.run() + + // Then + verify(mockFileMover).moveFiles(fakeFromDirectory, fakeToDirectory) + } + + @Test + fun `M retry W run() {move fails once}`() { + // Given + whenever(mockFileMover.moveFiles(fakeFromDirectory, fakeToDirectory)) + .doReturn(false, true) + + // When + testedOperation.run() + + // Then + verify(mockFileMover, times(2)).moveFiles(fakeFromDirectory, fakeToDirectory) + } + + @Test + fun `M retry with 500ms delay W run() {move fails once}`() { + // Given + whenever(mockFileMover.moveFiles(fakeFromDirectory, fakeToDirectory)) + .doReturn(false, true) + + // When + val duration = measureTimeMillis { + testedOperation.run() + } + + // Then + verify(mockFileMover, times(2)).moveFiles(fakeFromDirectory, fakeToDirectory) + assertThat(duration).isBetween(500L, 550L) + } + + @Test + fun `M try 3 times maximum W run() {move always fails}`() { + // Given + whenever(mockFileMover.moveFiles(fakeFromDirectory, fakeToDirectory)) + .doReturn(false) + + // When + testedOperation.run() + + // Then + verify(mockFileMover, times(3)).moveFiles(fakeFromDirectory, fakeToDirectory) + } + + @Test + fun `M retry with 500ms delay W run() {move always fails}`() { + // Given + whenever(mockFileMover.moveFiles(fakeFromDirectory, fakeToDirectory)) + .doReturn(false) + + // When + val duration = measureTimeMillis { + testedOperation.run() + } + + // Then + verify(mockFileMover, times(3)).moveFiles(fakeFromDirectory, fakeToDirectory) + assertThat(duration).isBetween(1000L, 1100L) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ScheduledWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ScheduledWriterTest.kt new file mode 100644 index 0000000000..95b3118c8b --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/ScheduledWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.DataWriter +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.ExecutorService +import java.util.concurrent.RejectedExecutionException + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ScheduledWriterTest { + + lateinit var testedWriter: DataWriter + + @Mock + lateinit var mockDelegateWriter: DataWriter + + @Mock + lateinit var mockExecutorService: ExecutorService + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedWriter = ScheduledWriter( + mockDelegateWriter, + mockExecutorService, + mockInternalLogger + ) + } + + @Test + fun `M schedule write W write(T)`( + @StringForgery data: String + ) { + // Given + + // When + testedWriter.write(data) + + // Then + verifyNoInteractions(mockDelegateWriter) + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDelegateWriter).write(data) + } + + verifyNoMoreInteractions(mockDelegateWriter, mockExecutorService) + } + + @Test + fun `M drop data and warn W write(T) {submit rejected}`( + @StringForgery data: String, + @StringForgery errorMessage: String + ) { + // Given + val exception = RejectedExecutionException(errorMessage) + whenever(mockExecutorService.execute(any())) doThrow exception + + // When + testedWriter.write(data) + + // Then + verifyNoInteractions(mockDelegateWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule Data writing task on the executor", + exception + ) + } + + @Test + fun `M schedule write W write(List)`( + @StringForgery data: List + ) { + // Given + + // When + testedWriter.write(data) + + // Then + verifyNoInteractions(mockDelegateWriter) + argumentCaptor { + verify(mockExecutorService).execute(capture()) + firstValue.run() + verify(mockDelegateWriter).write(data) + } + + verifyNoMoreInteractions(mockDelegateWriter, mockExecutorService) + } + + @Test + fun `M drop data and warn W write(List) {submit rejected}`( + @StringForgery data: List, + @StringForgery errorMessage: String + ) { + // Given + val exception = RejectedExecutionException(errorMessage) + whenever(mockExecutorService.execute(any())) doThrow exception + + // When + testedWriter.write(data) + + // Then + verifyNoInteractions(mockDelegateWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule Data writing task on the executor", + exception + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/SingleFileOrchestratorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/SingleFileOrchestratorTest.kt new file mode 100644 index 0000000000..fdb6f65b95 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/SingleFileOrchestratorTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.single.SingleFileOrchestrator +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class SingleFileOrchestratorTest { + + private lateinit var testedOrchestrator: SingleFileOrchestrator + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @TempDir + lateinit var tempDir: File + + @StringForgery + lateinit var fakeParentDirName: String + + @StringForgery + lateinit var fakeFileName: String + + lateinit var fakeFile: File + + @BeforeEach + fun `set up`() { + fakeFile = File(File(tempDir, fakeParentDirName), fakeFileName) + testedOrchestrator = SingleFileOrchestrator(fakeFile, mockInternalLogger) + } + + // region getWritableFile + + @Test + fun `M create parent dir W getWritableFile()`() { + // When + testedOrchestrator.getWritableFile() + + // Then + assertThat(fakeFile.parentFile).exists() + } + + @Test + fun `M return file W getWritableFile()`() { + // When + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isSameAs(fakeFile) + } + + // endregion + + // region getReadableFile + + @Test + fun `M create parent dir W getReadableFile()`() { + // When + testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(fakeFile.parentFile).exists() + } + + @Test + fun `M return file W getReadableFile()`() { + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isSameAs(fakeFile) + } + + @Test + fun `M return null W getReadableFile() {file is excluded}`() { + // When + val result = testedOrchestrator.getReadableFile(setOf(fakeFile)) + + // Then + assertThat(result).isNull() + } + + // endregion + + // region getAllFiles + + @Test + fun `M create parent dir W getAllFiles()`() { + // When + testedOrchestrator.getAllFiles() + + // Then + assertThat(fakeFile.parentFile).exists() + } + + @Test + fun `M return file W getAllFiles()`() { + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result) + .contains(fakeFile) + .hasSize(1) + } + + @Test + fun `M return file W getAllFlushableFiles()`() { + // When + val result = testedOrchestrator.getFlushableFiles() + + // Then + assertThat(result) + .contains(fakeFile) + .hasSize(1) + } + + // endregion + + // region getRootDir + + @Test + fun `M return null W getRootDir()`() { + // When + val result = testedOrchestrator.getRootDir() + + // Then + assertThat(result).isNull() + } + + // endregion + + // region getRootDirName + + @Test + fun `M return file parent dirname W getRootDirName()`() { + // When + val result = testedOrchestrator.getRootDirName() + + // Then + assertThat(result).isEqualTo(fakeParentDirName) + } + + @Test + fun `M return null W getRootDirName() { parent dir is null }`() { + // Given + val fakeInvalidFile: File = mock() + testedOrchestrator = SingleFileOrchestrator(fakeInvalidFile, mockInternalLogger) + + // When + val result = testedOrchestrator.getRootDirName() + + // Then + assertThat(result).isNull() + } + + // endregion + + // region getMetadataFile + + @Test + fun `M return null W getMetadataFile()`() { + // When + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isNull() + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperationTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperationTest.kt new file mode 100644 index 0000000000..f6a9dbab4b --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperationTest.kt @@ -0,0 +1,134 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.advanced + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import kotlin.system.measureTimeMillis + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class WipeDataMigrationOperationTest { + + lateinit var testedOperation: DataMigrationOperation + + @TempDir + lateinit var fakeTargetDirectory: File + + @Mock + lateinit var mockFileMover: FileMover + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedOperation = WipeDataMigrationOperation( + fakeTargetDirectory, + mockFileMover, + mockInternalLogger + ) + } + + @Test + fun `M warn W run() {dir is null}`() { + // Given + testedOperation = WipeDataMigrationOperation( + null, + mockFileMover, + mockInternalLogger + ) + + // When + testedOperation.run() + + // Then + verifyNoInteractions(mockFileMover) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + WipeDataMigrationOperation.WARN_NULL_DIR + ) + } + + @Test + fun `M delete dir recursively W run()`() { + // Given + whenever(mockFileMover.delete(fakeTargetDirectory)) doReturn true + + // When + testedOperation.run() + + // Then + verify(mockFileMover).delete(fakeTargetDirectory) + } + + @Test + fun `M retry W run() {delete fails once}`() { + // Given + whenever(mockFileMover.delete(fakeTargetDirectory)).doReturn(false, true) + + // When + testedOperation.run() + + // Then + verify(mockFileMover, times(2)).delete(fakeTargetDirectory) + } + + @Test + fun `M try 3 times maximum W run() {move always fails}`() { + // Given + whenever(mockFileMover.delete(fakeTargetDirectory)) + .doReturn(false) + + // When + testedOperation.run() + + // Then + verify(mockFileMover, times(3)).delete(fakeTargetDirectory) + } + + @Test + fun `M retry with 500ms delay W run() {move always fails}`() { + // Given + whenever(mockFileMover.delete(fakeTargetDirectory)) + .doReturn(false) + + // When + val duration = measureTimeMillis { + testedOperation.run() + } + + // Then + verify(mockFileMover, times(3)).delete(fakeTargetDirectory) + assertThat(duration).isBetween(1000L, 1100L) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileDataReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileDataReaderTest.kt new file mode 100644 index 0000000000..5cbe51b9ae --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileDataReaderTest.kt @@ -0,0 +1,444 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.persistence.Batch +import com.datadog.android.core.internal.persistence.DataReader +import com.datadog.android.core.internal.persistence.PayloadDecoration +import com.datadog.android.core.internal.persistence.file.FileMover +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.Locale +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class BatchFileDataReaderTest { + + lateinit var testedReader: DataReader + + @Mock + lateinit var mockOrchestrator: FileOrchestrator + + @Mock + lateinit var mockFileReader: BatchFileReader + + @Mock + lateinit var mockFileMover: FileMover + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Forgery + lateinit var fakeDecoration: PayloadDecoration + + @BeforeEach + fun `set up`() { + testedReader = BatchFileDataReader( + mockOrchestrator, + fakeDecoration, + mockFileReader, + mockFileMover, + mockInternalLogger + ) + } + + // region lockAndReadNext + + @Test + fun `M read batch W lockAndReadNext()`( + @Forgery fakeData: List, + @Forgery file: File + ) { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file + whenever( + mockFileReader.readData(file) + ) doReturn fakeData + + // When + val result = testedReader.lockAndReadNext() + + // Then + checkNotNull(result) + assertThat(result.id).isEqualTo(file.name) + assertThat(result.data).isEqualTo( + fakeData.map { it.data }.join( + fakeDecoration.separatorBytes, + fakeDecoration.prefixBytes, + fakeDecoration.suffixBytes + ) + ) + } + + @Test + fun `M return null W lockAndReadNext() {no file}`() { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn null + + // When + val result = testedReader.lockAndReadNext() + + // Then + assertThat(result).isNull() + } + + // endregion + + // region release + + @Test + fun `M read batch twice W lockAndReadNext() + release() + lockAndReadNext()`( + @Forgery fakeData: List, + @Forgery file: File + ) { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file + whenever( + mockFileReader.readData(file) + ) doReturn fakeData + + // When + val result1 = testedReader.lockAndReadNext() + checkNotNull(result1) + testedReader.release(result1) + val result2 = testedReader.lockAndReadNext() + + // Then + checkNotNull(result2) + assertThat(result2.id).isEqualTo(file.name) + assertThat(result2.data).isEqualTo( + fakeData.map { it.data }.join( + fakeDecoration.separatorBytes, + fakeDecoration.prefixBytes, + fakeDecoration.suffixBytes + ) + ) + verify(mockFileMover, never()).delete(any()) + } + + @Test + fun `M read batch twice W lockAndReadNext() + release() + lockAndReadNext() {multithreaded}`( + @Forgery fakeData: List, + @Forgery file: File + ) { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file + whenever( + mockFileReader.readData(file) + ) doReturn fakeData + val countDownLatch = CountDownLatch(2) + + // When + val result1 = testedReader.lockAndReadNext() + checkNotNull(result1) + var threadResult: Batch? = null + Thread { + Thread.sleep(100) + threadResult = testedReader.lockAndReadNext() + countDownLatch.countDown() + }.start() + Thread { + testedReader.release(result1) + countDownLatch.countDown() + }.start() + + // Then + countDownLatch.await(500, TimeUnit.MILLISECONDS) + val result2 = threadResult + checkNotNull(result2) + assertThat(result2.id).isEqualTo(file.name) + assertThat(result2.data).isEqualTo( + fakeData.map { it.data }.join( + fakeDecoration.separatorBytes, + fakeDecoration.prefixBytes, + fakeDecoration.suffixBytes + ) + ) + verify(mockFileMover, never()).delete(any()) + } + + @Test + fun `M read batch once W lockAndReadNext() + release() {diff} + lockAndReadNext()`( + @Forgery fakeData: List, + @Forgery file: File + ) { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file + whenever( + mockFileReader.readData(file) + ) doReturn fakeData + + // When + val result1 = testedReader.lockAndReadNext() + testedReader.release(Batch(file.name.reversed() + "0", ByteArray(0))) + val result2 = testedReader.lockAndReadNext() + + // Then + checkNotNull(result1) + assertThat(result1.id).isEqualTo(file.name) + assertThat(result1.data).isEqualTo( + fakeData.map { it.data }.join( + fakeDecoration.separatorBytes, + fakeDecoration.prefixBytes, + fakeDecoration.suffixBytes + ) + ) + assertThat(result2).isNull() + verify(mockFileMover, never()).delete(any()) + } + + @Test + fun `M read and release multiple batches W lockAndReadNext() + release() { multithreaded }`( + @Forgery fakeData: List, + @Forgery file1: File, + @Forgery file2: File, + @Forgery file3: File, + @Forgery file4: File + ) { + // Given + val files = listOf(file1, file2, file3, file4) + val expectedIds = files.map { it.name } + whenever(mockOrchestrator.getReadableFile(any())) doAnswer { invocation -> + val set = invocation.getArgument>(0) + files.first { it.name !in set } + } + whenever(mockFileReader.readData(any())) doReturn fakeData + val countDownLatch = CountDownLatch(4) + + // When + val results = mutableListOf() + repeat(4) { + Thread { + val result = testedReader.lockAndReadNext() + results.add(result) + if (result != null) { + testedReader.release(result) + } + countDownLatch.countDown() + }.start() + } + + // Then + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(results) + .hasSize(4) + .doesNotContainNull() + .allMatch { it!!.id in expectedIds } + verify(mockFileMover, never()).delete(any()) + } + + @Test + fun `M warn W release() unknown file`( + @StringForgery fileName: String + ) { + // Given + val data = Batch(fileName, ByteArray(0)) + + // When + testedReader.release(data) + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + BatchFileDataReader.WARNING_UNKNOWN_BATCH_ID.format(Locale.US, fileName) + ) + } + + // endregion + + // region drop + + @Test + fun `M delete underlying file+meta W lockAndReadNext() + dropBatch()`( + @Forgery fakeData: List, + @Forgery file: File, + forge: Forge + ) { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file + val metaFileMock = mock().apply { + whenever(exists()) doReturn true + whenever(path) doReturn "${file.path}_${forge.anAlphabeticalString()}" + } + whenever(mockOrchestrator.getMetadataFile(file)) doReturn metaFileMock + whenever( + mockFileReader.readData(file) + ) doReturn fakeData + whenever(mockFileMover.delete(file)) doReturn true + + // Then + val result = testedReader.lockAndReadNext() + checkNotNull(result) + testedReader.drop(result) + + // Then + verify(mockFileMover).delete(file) + verify(mockFileMover).delete(metaFileMock) + } + + @Test + fun `M warn W lockAndReadNext() + dropBatch() {delete fails}`( + @Forgery fakeData: List, + @Forgery file: File + ) { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file + whenever( + mockFileReader.readData(file) + ) doReturn fakeData + whenever(mockFileMover.delete(file)) doReturn false + + // Then + val result = testedReader.lockAndReadNext() + checkNotNull(result) + testedReader.drop(result) + + // Then + verify(mockFileMover).delete(file) + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + BatchFileDataReader.WARNING_DELETE_FAILED.format(Locale.US, file.path) + ) + } + + @Test + fun `M warn W drop() unknown file`( + @StringForgery fileName: String + ) { + // Given + val data = Batch(fileName, ByteArray(0)) + + // When + testedReader.drop(data) + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + BatchFileDataReader.WARNING_UNKNOWN_BATCH_ID.format(Locale.US, fileName) + ) + } + + // endregion + + // region dropAll + + @Test + fun `M delete underlying file W lockAndReadNext() + dropAll()`( + @Forgery fakeData: List, + @Forgery file: File, + forge: Forge + ) { + // Given + whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file + val metaFileMock = mock().apply { + whenever(exists()) doReturn true + whenever(path) doReturn "${file.path}_${forge.anAlphabeticalString()}" + } + whenever(mockOrchestrator.getMetadataFile(file)) doReturn metaFileMock + whenever(mockOrchestrator.getAllFiles()) doReturn emptyList() + whenever( + mockFileReader.readData(file) + ) doReturn fakeData + whenever(mockFileMover.delete(file)) doReturn true + + // Then + val result = testedReader.lockAndReadNext() + checkNotNull(result) + testedReader.dropAll() + + // Then + verify(mockFileMover).delete(file) + verify(mockFileMover).delete(metaFileMock) + } + + @Test + fun `M delete all files+meta W lockAndReadNext() + dropAll()`( + @Forgery file1: File, + @Forgery file2: File, + @Forgery file3: File, + @Forgery file4: File, + forge: Forge + ) { + // Given + val files = listOf(file1, file2, file3, file4) + whenever(mockOrchestrator.getAllFiles()) doReturn files + whenever(mockFileMover.delete(any())) doReturn true + + val metaFileMocks = files.map { + mock().apply { + whenever(exists()) doReturn true + whenever(path) doReturn "${it.path}_${forge.anAlphabeticalString()}" + } + } + + whenever(mockOrchestrator.getMetadataFile(any())) doReturnConsecutively metaFileMocks + + // Then + testedReader.dropAll() + + // Then + verify(mockFileMover).delete(file1) + verify(mockFileMover).delete(metaFileMocks[0]) + verify(mockFileMover).delete(file2) + verify(mockFileMover).delete(metaFileMocks[1]) + verify(mockFileMover).delete(file3) + verify(mockFileMover).delete(metaFileMocks[2]) + verify(mockFileMover).delete(file4) + verify(mockFileMover).delete(metaFileMocks[3]) + } + + // endregion + + // region private + + private fun List.join( + separator: ByteArray, + prefix: ByteArray, + suffix: ByteArray + ): ByteArray { + return prefix + this.reduce { acc, bytes -> + acc + separator + bytes + } + suffix + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestratorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestratorTest.kt new file mode 100644 index 0000000000..8d83ceeee4 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestratorTest.kt @@ -0,0 +1,1224 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.metrics.BatchClosedMetadata +import com.datadog.android.core.internal.metrics.MetricsDispatcher +import com.datadog.android.core.internal.metrics.RemovalReason +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.Locale +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class BatchFileOrchestratorTest { + + private lateinit var testedOrchestrator: FileOrchestrator + + @TempDir + lateinit var tempDir: File + + @Mock + lateinit var mockLogger: InternalLogger + + @StringForgery + lateinit var fakeRootDirName: String + + lateinit var fakeRootDir: File + + @Mock + lateinit var mockMetricsDispatcher: MetricsDispatcher + + @IntForgery(min = 0, max = 100) + var fakePendingBatches: Int = 0 + + @Mock + lateinit var mockPendingFiles: AtomicInteger + + @BeforeEach + fun `set up`() { + whenever(mockPendingFiles.decrementAndGet()).thenReturn(fakePendingBatches) + whenever(mockPendingFiles.incrementAndGet()).thenReturn(fakePendingBatches) + fakeRootDir = File(tempDir, fakeRootDirName) + fakeRootDir.mkdirs() + testedOrchestrator = BatchFileOrchestrator( + rootDir = fakeRootDir, + config = TEST_PERSISTENCE_CONFIG, + internalLogger = mockLogger, + metricsDispatcher = mockMetricsDispatcher, + pendingFiles = mockPendingFiles + ) + } + + // region getWritableFile + + @Test + fun `M not send batch_closed metric W getWritableFile() {no prev file}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + checkNotNull(result) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M send batch_closed metric W getWritableFile()`() { + // Given + val lowerTimestamp = System.currentTimeMillis() + val oldFile = testedOrchestrator.getWritableFile() + val upperTimestamp = System.currentTimeMillis() + Thread.sleep(RECENT_DELAY_MS + 1) + + // When + testedOrchestrator.getWritableFile() + + // Then + val fileArgumentCaptor = argumentCaptor() + val metadataArgumentCaptor = argumentCaptor() + verify(mockMetricsDispatcher).sendBatchClosedMetric( + fileArgumentCaptor.capture(), + metadataArgumentCaptor.capture() + ) + + assertThat(fileArgumentCaptor.firstValue).isEqualTo(oldFile) + metadataArgumentCaptor.firstValue.let { + assertThat(it.eventsCount).isEqualTo(1L) + assertThat(it.lastTimeWasUsedInMs) + .isBetween(lowerTimestamp, upperTimestamp) + } + verifyNoMoreInteractions(mockMetricsDispatcher) + } + + @Test + fun `M warn W getWritableFile() {root is not a dir}`( + @StringForgery fileName: String + ) { + // Given + val notADir = File(fakeRootDir, fileName) + notADir.createNewFile() + testedOrchestrator = BatchFileOrchestrator( + notADir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_DIR.format(Locale.US, notADir.path) + ) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M warn W getWritableFile() {root can't be created}`() { + // Given + val corruptedDir = mock() + whenever(corruptedDir.exists()).thenReturn(false) + whenever(corruptedDir.mkdirs()).thenReturn(false) + whenever(corruptedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + corruptedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_CANT_CREATE_ROOT.format(Locale.US, fakeRootDir.path) + ) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M warn W getWritableFile() {root is not writeable}`() { + // Given + val restrictedDir = mock() + whenever(restrictedDir.exists()).thenReturn(true) + whenever(restrictedDir.isDirectory).thenReturn(true) + whenever(restrictedDir.canWrite()).thenReturn(false) + whenever(restrictedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + restrictedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_WRITABLE.format(Locale.US, fakeRootDir.path) + ) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M create the rootDirectory W getWritableFile() {root does not exist}`() { + // Given + fakeRootDir.deleteRecursively() + + // When + testedOrchestrator.getWritableFile() + + // Then + assertThat(fakeRootDir).exists().isDirectory() + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M delete obsolete files W getWritableFile()`( + @LongForgery(min = OLD_FILE_THRESHOLD, max = Int.MAX_VALUE.toLong()) oldFileAge: Long + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val oldTimestamp = System.currentTimeMillis() - oldFileAge + val oldFile = File(fakeRootDir, oldTimestamp.toString()) + oldFile.createNewFile() + val oldFileMeta = File("${oldFile.path}_metadata") + oldFileMeta.createNewFile() + val youngTimestamp = System.currentTimeMillis() - RECENT_DELAY_MS - 1 + val youngFile = File(fakeRootDir, youngTimestamp.toString()) + youngFile.createNewFile() + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(oldFile).doesNotExist() + assertThat(oldFileMeta).doesNotExist() + assertThat(youngFile).exists() + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(oldFile), + argThat { this is RemovalReason.Obsolete }, + eq(fakePendingBatches) + ) + verifyNoMoreInteractions(mockMetricsDispatcher) + } + + @Test + fun `M respect time threshold to delete obsolete files W getWritableFile() { below threshold }`( + @LongForgery(min = OLD_FILE_THRESHOLD, max = Int.MAX_VALUE.toLong()) oldFileAge: Long + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val oldTimestamp = System.currentTimeMillis() - oldFileAge + val oldFile = File(fakeRootDir, oldTimestamp.toString()) + oldFile.createNewFile() + val oldFileMeta = File("${oldFile.path}_metadata") + oldFileMeta.createNewFile() + val youngTimestamp = System.currentTimeMillis() - RECENT_DELAY_MS - 1 + val youngFile = File(fakeRootDir, youngTimestamp.toString()) + youngFile.createNewFile() + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + // let's add very old file after the previous cleanup call. If threshold is respected, + // cleanup shouldn't be performed during the next getWritableFile call + val evenOlderFile = File(fakeRootDir, (oldTimestamp - 1).toString()) + evenOlderFile.createNewFile() + testedOrchestrator.getWritableFile() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(oldFile).doesNotExist() + assertThat(oldFileMeta).doesNotExist() + assertThat(youngFile).exists() + assertThat(evenOlderFile).exists() + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(oldFile), + argThat { this is RemovalReason.Obsolete }, + eq(fakePendingBatches) + ) + } + + @Test + fun `M respect time threshold to delete obsolete files W getWritableFile() { above threshold }`( + @LongForgery(min = OLD_FILE_THRESHOLD, max = Int.MAX_VALUE.toLong()) oldFileAge: Long + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val oldTimestamp = System.currentTimeMillis() - oldFileAge + val oldFile = File(fakeRootDir, oldTimestamp.toString()) + oldFile.createNewFile() + val oldFileMeta = File("${oldFile.path}_metadata") + oldFileMeta.createNewFile() + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + Thread.sleep(CLEANUP_FREQUENCY_THRESHOLD_MS + 1) + val evenOlderFile = File(fakeRootDir, (oldTimestamp - 1).toString()) + evenOlderFile.createNewFile() + testedOrchestrator.getWritableFile() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(oldFile).doesNotExist() + assertThat(oldFileMeta).doesNotExist() + assertThat(evenOlderFile).doesNotExist() + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(evenOlderFile), + argThat { this is RemovalReason.Obsolete }, + eq(fakePendingBatches) + ) + verify(mockMetricsDispatcher).sendBatchDeletedMetric( + eq(oldFile), + argThat { this is RemovalReason.Obsolete }, + eq(fakePendingBatches) + ) + argumentCaptor() { + verify(mockMetricsDispatcher).sendBatchClosedMetric( + eq(result), + capture() + ) + assertThat(firstValue.eventsCount).isEqualTo(1L) + assertThat(firstValue.lastTimeWasUsedInMs) + .isBetween(start, end) + } + verifyNoMoreInteractions(mockMetricsDispatcher) + } + + @Test + fun `M return new File W getWritableFile() {no available file}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M return existing File W getWritableFile() {recent file exist with spare space}`( + @StringForgery(size = SMALL_ITEM_SIZE) previousData: String + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val previousFile = testedOrchestrator.getWritableFile() + checkNotNull(previousFile) + previousFile.writeText(previousData) + Thread.sleep(1) + + // When + val result = testedOrchestrator.getWritableFile() + + // Then + checkNotNull(result) + assertThat(result).isEqualTo(previousFile) + assertThat(previousFile.readText()).isEqualTo(previousData) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M return new File W getWritableFile() {previous file is too old}`( + @StringForgery(size = SMALL_ITEM_SIZE) previousData: String + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val beforeFileCreateTimestamp = System.currentTimeMillis() + val previousFile = testedOrchestrator.getWritableFile() + val afterFileCreateTimestamp = System.currentTimeMillis() + + checkNotNull(previousFile) + previousFile.writeText(previousData) + Thread.sleep(RECENT_DELAY_MS + 1) + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(previousFile.readText()).isEqualTo(previousData) + argumentCaptor() { + verify(mockMetricsDispatcher).sendBatchClosedMetric(eq(previousFile), capture()) + assertThat(firstValue.lastTimeWasUsedInMs) + .isBetween(beforeFileCreateTimestamp, afterFileCreateTimestamp) + assertThat(firstValue.eventsCount).isEqualTo(1L) + } + verifyNoMoreInteractions(mockMetricsDispatcher) + } + + @Test + fun `M return new File W getWritableFile() {previous file is unknown}`( + @StringForgery(size = SMALL_ITEM_SIZE) previousData: String + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val previousFile = File(fakeRootDir, System.currentTimeMillis().toString()) + previousFile.writeText(previousData) + Thread.sleep(1) + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(previousFile.readText()).isEqualTo(previousData) + verifyNoInteractions(mockMetricsDispatcher) + } + + @Test + fun `M return new File W getWritableFile() {previous file is deleted}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val beforeFileCreateTimestamp = System.currentTimeMillis() + val previousFile = testedOrchestrator.getWritableFile() + val afterFileCreateTimestamp = System.currentTimeMillis() + checkNotNull(previousFile) + previousFile.createNewFile() + previousFile.delete() + Thread.sleep(1) + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(previousFile).doesNotExist() + argumentCaptor() { + verify(mockMetricsDispatcher).sendBatchClosedMetric(eq(previousFile), capture()) + assertThat(firstValue.lastTimeWasUsedInMs) + .isBetween(beforeFileCreateTimestamp, afterFileCreateTimestamp) + assertThat(firstValue.eventsCount).isEqualTo(1L) + } + verifyNoMoreInteractions(mockMetricsDispatcher) + } + + @Test + fun `M return new File W getWritableFile() {previous file is too large}`( + @StringForgery(size = MAX_BATCH_SIZE) previousData: String + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val beforeFileCreateTimestamp = System.currentTimeMillis() + val previousFile = testedOrchestrator.getWritableFile() + val afterFileCreateTimestamp = System.currentTimeMillis() + checkNotNull(previousFile) + previousFile.writeText(previousData) + Thread.sleep(1) + + // When + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(previousFile.readText()).isEqualTo(previousData) + argumentCaptor() { + verify(mockMetricsDispatcher).sendBatchClosedMetric(eq(previousFile), capture()) + assertThat(firstValue.lastTimeWasUsedInMs) + .isBetween(beforeFileCreateTimestamp, afterFileCreateTimestamp) + assertThat(firstValue.eventsCount).isEqualTo(1L) + } + verifyNoMoreInteractions(mockMetricsDispatcher) + } + + @Test + fun `M return new File W getWritableFile() {previous file has too many items}`( + forge: Forge + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val beforeFileCreateTimestamp = System.currentTimeMillis() + var previousFile = testedOrchestrator.getWritableFile() + + repeat(4) { + checkNotNull(previousFile) + + val previousData = forge.aList(MAX_ITEM_PER_BATCH) { + forge.anAlphabeticalString() + } + + previousFile?.writeText(previousData[0]) + + for (i in 1 until MAX_ITEM_PER_BATCH) { + val file = testedOrchestrator.getWritableFile() + assumeTrue(file == previousFile) + file?.appendText(previousData[i]) + } + val afterLastFileUsageTimestamp = System.currentTimeMillis() + + // When + val start = System.currentTimeMillis() + val nextFile = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(nextFile) + assertThat(nextFile) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(nextFile.name.toLong()) + .isBetween(start, end) + assertThat(previousFile?.readText()) + .isEqualTo(previousData.joinToString(separator = "")) + + argumentCaptor() { + verify(mockMetricsDispatcher).sendBatchClosedMetric(eq(previousFile!!), capture()) + assertThat(firstValue.lastTimeWasUsedInMs) + .isBetween(beforeFileCreateTimestamp, afterLastFileUsageTimestamp) + assertThat(firstValue.eventsCount).isEqualTo(MAX_ITEM_PER_BATCH.toLong()) + } + previousFile = nextFile + } + verifyNoMoreInteractions(mockMetricsDispatcher) + } + + @Test + fun `M discard File W getWritableFile() {previous files take too much disk space}`( + @StringForgery(size = MAX_BATCH_SIZE) previousData: String + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val filesCount = MAX_DISK_SPACE / MAX_BATCH_SIZE + val files = (0..filesCount).map { + val file = testedOrchestrator.getWritableFile() + checkNotNull(file) + file.writeText(previousData) + Thread.sleep(1) + file + } + + // When + Thread.sleep(CLEANUP_FREQUENCY_THRESHOLD_MS + 1) + val start = System.currentTimeMillis() + val result = testedOrchestrator.getWritableFile() + val end = System.currentTimeMillis() + + // Then + checkNotNull(result) + assertThat(result) + .doesNotExist() + .hasParent(fakeRootDir) + assertThat(result.name.toLong()) + .isBetween(start, end) + assertThat(files.first()).doesNotExist() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_DISK_FULL.format( + Locale.US, + files.size * previousData.length, + MAX_DISK_SPACE, + (files.size * previousData.length) - MAX_DISK_SPACE + ) + ) + } + + // endregion + + // region getReadableFile + + @Test + fun `M warn W getReadableFile() {root is not a dir}`( + @StringForgery fileName: String + ) { + // Given + val notADir = File(fakeRootDir, fileName) + notADir.createNewFile() + testedOrchestrator = BatchFileOrchestrator( + notADir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_DIR.format(Locale.US, notADir.path) + ) + } + + @Test + fun `M warn W getReadableFile() {root can't be created}`() { + // Given + val corruptedDir = mock() + whenever(corruptedDir.exists()).thenReturn(false) + whenever(corruptedDir.mkdirs()).thenReturn(false) + whenever(corruptedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + corruptedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_CANT_CREATE_ROOT.format(Locale.US, fakeRootDir.path) + ) + } + + @Test + fun `M warn W getReadableFile() {root is not writeable}`() { + // Given + val restrictedDir = mock() + whenever(restrictedDir.exists()).thenReturn(true) + whenever(restrictedDir.isDirectory).thenReturn(true) + whenever(restrictedDir.canWrite()).thenReturn(false) + whenever(restrictedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + restrictedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_WRITABLE.format(Locale.US, fakeRootDir.path) + ) + } + + @Test + fun `M delete obsolete files W getReadableFile()`( + @LongForgery(min = OLD_FILE_THRESHOLD, max = Int.MAX_VALUE.toLong()) oldFileAge: Long + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val oldTimestamp = System.currentTimeMillis() - oldFileAge + val oldFile = File(fakeRootDir, oldTimestamp.toString()) + oldFile.createNewFile() + val oldFileMeta = File("${oldFile.path}_metadata") + oldFileMeta.createNewFile() + val youngTimestamp = System.currentTimeMillis() - RECENT_DELAY_MS - 1 + val youngFile = File(fakeRootDir, youngTimestamp.toString()) + youngFile.createNewFile() + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isNull() + assertThat(oldFile).doesNotExist() + assertThat(oldFileMeta).doesNotExist() + assertThat(youngFile).exists() + } + + @Test + fun `M create the rootDirectory W getReadableFile() {root does not exist}`() { + // Given + fakeRootDir.deleteRecursively() + + // When + testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(fakeRootDir).exists().isDirectory() + } + + @Test + fun `M return null W getReadableFile() {empty dir}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return file W getReadableFile() {existing old enough file}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val timestamp = System.currentTimeMillis() - (RECENT_DELAY_MS * 2) + val file = File(fakeRootDir, timestamp.toString()) + file.createNewFile() + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result) + .isEqualTo(file) + .exists() + .hasContent("") + } + + @Test + fun `M return null W getReadableFile() {file is too recent}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val timestamp = System.currentTimeMillis() - (RECENT_DELAY_MS / 2) + val file = File(fakeRootDir, timestamp.toString()) + file.createNewFile() + + // When + val result = testedOrchestrator.getReadableFile(emptySet()) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W getReadableFile() {file is in exclude list}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val timestamp = System.currentTimeMillis() - (RECENT_DELAY_MS * 2) + val file = File(fakeRootDir, timestamp.toString()) + file.createNewFile() + + // When + val result = testedOrchestrator.getReadableFile(setOf(file)) + + // Then + assertThat(result).isNull() + } + + // endregion + + // region getAllFiles + + @Test + fun `M warn W getAllFiles() {root is not a dir}`( + @StringForgery fileName: String + ) { + // Given + val notADir = File(fakeRootDir, fileName) + notADir.createNewFile() + testedOrchestrator = BatchFileOrchestrator( + notADir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result).isEmpty() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_DIR.format(Locale.US, notADir.path) + ) + } + + @Test + fun `M warn W getAllFiles() {root can't be created}`() { + // Given + val corruptedDir = mock() + whenever(corruptedDir.exists()).thenReturn(false) + whenever(corruptedDir.mkdirs()).thenReturn(false) + whenever(corruptedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + corruptedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result).isEmpty() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_CANT_CREATE_ROOT.format(Locale.US, fakeRootDir.path) + ) + } + + @Test + fun `M warn W getAllFiles() {root is not writeable}`() { + // Given + val restrictedDir = mock() + whenever(restrictedDir.exists()).thenReturn(true) + whenever(restrictedDir.isDirectory).thenReturn(true) + whenever(restrictedDir.canWrite()).thenReturn(false) + whenever(restrictedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + restrictedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result).isEmpty() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_WRITABLE.format(Locale.US, fakeRootDir.path) + ) + } + + @Test + fun `M create the rootDirectory W getAllFiles() {root does not exist}`() { + // Given + fakeRootDir.deleteRecursively() + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result).isEmpty() + assertThat(fakeRootDir).exists().isDirectory() + } + + @Test + fun `M return empty list W getAllFiles() {dir is empty}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result).isEmpty() + } + + @Test + fun `M return all files W getAllFiles() {dir is not empty}`( + @IntForgery(1, 32) count: Int + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val old = System.currentTimeMillis() - (RECENT_DELAY_MS * 2) + val new = System.currentTimeMillis() - (RECENT_DELAY_MS / 2) + val expectedFiles = mutableListOf() + for (i in 1..count) { + // create both non readable and non writable files + expectedFiles.add( + File(fakeRootDir, (new + i).toString()).also { it.createNewFile() } + ) + expectedFiles.add( + File(fakeRootDir, (old - i).toString()).also { it.createNewFile() } + ) + } + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result).containsAll(expectedFiles) + } + + @Test + fun `M return empty list W getAllFiles() {dir files don't match pattern}`( + @StringForgery fileName: String + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val file = File(fakeRootDir, fileName) + file.createNewFile() + + // When + val result = testedOrchestrator.getAllFiles() + + // Then + assertThat(result).isEmpty() + } + + // endregion + + // region getAllFlushableFiles + + @Test + fun `M return all files W getAllFlushableFiles() {dir is not empty}`( + @IntForgery(1, 32) count: Int + ) { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + val old = System.currentTimeMillis() - (RECENT_DELAY_MS * 2) + val new = System.currentTimeMillis() - (RECENT_DELAY_MS / 2) + val expectedFiles = mutableListOf() + for (i in 1..count) { + // create both non readable and non writable files + expectedFiles.add( + File(fakeRootDir, (new + i).toString()).also { it.createNewFile() } + ) + expectedFiles.add( + File(fakeRootDir, (old - i).toString()).also { it.createNewFile() } + ) + } + + // When + val result = testedOrchestrator.getFlushableFiles() + + // Then + assertThat(result).containsAll(expectedFiles) + } + + @Test + fun `M return empty list W getAllFlushableFiles() {dir is empty}`() { + // Given + assumeTrue(fakeRootDir.listFiles().isNullOrEmpty()) + + // When + val result = testedOrchestrator.getFlushableFiles() + + // Then + assertThat(result).isEmpty() + } + + // endregion + + // region getRootDir + + @Test + fun `M warn W getRootDir() {root is not a dir}`( + @StringForgery fileName: String + ) { + // Given + val notADir = File(fakeRootDir, fileName) + notADir.createNewFile() + testedOrchestrator = BatchFileOrchestrator( + notADir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getRootDir() + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_DIR.format(Locale.US, notADir.path) + ) + } + + @Test + fun `M warn W getRootDir() {root can't be created}`() { + // Given + val corruptedDir = mock() + whenever(corruptedDir.exists()).thenReturn(false) + whenever(corruptedDir.mkdirs()).thenReturn(false) + whenever(corruptedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + corruptedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getRootDir() + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_CANT_CREATE_ROOT.format(Locale.US, fakeRootDir.path) + ) + } + + @Test + fun `M warn W getRootDir() {root is not writeable}`() { + // Given + val restrictedDir = mock() + whenever(restrictedDir.exists()).thenReturn(true) + whenever(restrictedDir.isDirectory).thenReturn(true) + whenever(restrictedDir.canWrite()).thenReturn(false) + whenever(restrictedDir.path) doReturn fakeRootDir.path + testedOrchestrator = BatchFileOrchestrator( + restrictedDir, + TEST_PERSISTENCE_CONFIG, + mockLogger, + mockMetricsDispatcher + ) + + // When + val result = testedOrchestrator.getRootDir() + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_ROOT_NOT_WRITABLE.format(Locale.US, fakeRootDir.path) + ) + } + + @Test + fun `M return root dir`() { + // When + val result = testedOrchestrator.getRootDir() + + // Then + assertThat(result).isEqualTo(fakeRootDir) + verifyNoInteractions(mockLogger) + } + + @Test + fun `M return root dir { multithreaded }`( + @IntForgery(4, 8) repeatCount: Int + ) { + // since getRootDir involves the creation of the directory structure, + // we need to make sure that other threads won't try to create it again when it is already + // created by some thread + + // Given + fakeRootDir.deleteRecursively() + val countDownLatch = CountDownLatch(repeatCount) + val results = mutableListOf() + + // When + repeat(repeatCount) { + Thread { + val result = testedOrchestrator.getRootDir() + synchronized(results) { results.add(result) } + countDownLatch.countDown() + }.start() + } + countDownLatch.await(5, TimeUnit.SECONDS) + + // Then + assertThat(countDownLatch.count).isZero() + assertThat(results) + .hasSize(repeatCount) + .containsOnly(fakeRootDir) + verifyNoInteractions(mockLogger) + } + + // endregion + + // region getRootDirName + + @Test + fun `M return rootDirName W getRootDirName()`() { + // When + val result = testedOrchestrator.getRootDirName() + + // Then + assertThat(result).isEqualTo(fakeRootDir.nameWithoutExtension) + } + + // endregion + + // region getMetadataFile + + @Test + fun `M return metadata file W getMetadataFile()`() { + // Given + val fakeFileName = System.currentTimeMillis().toString() + val fakeFile = File(fakeRootDir.path, fakeFileName) + + // When + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isNotNull() + assertThat(result!!.name).isEqualTo("${fakeFileName}_metadata") + } + + @Test + fun `M log debug file W getMetadataFile() { file is from another folder }`( + @StringForgery fakeSuffix: String + ) { + // Given + val fakeFileName = System.currentTimeMillis().toString() + val fakeFile = File("${fakeRootDir.parent}$fakeSuffix", fakeFileName) + + // When + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isNotNull() + mockLogger.verifyLog( + InternalLogger.Level.DEBUG, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.DEBUG_DIFFERENT_ROOT + .format(Locale.US, fakeFile.path, fakeRootDir.path) + ) + } + + @Test + fun `M log error file W getMetadataFile() { not batch file argument }`( + @StringForgery fakeFileName: String + ) { + // Given + val fakeFile = File(fakeRootDir.path, fakeFileName) + + // When + val result = testedOrchestrator.getMetadataFile(fakeFile) + + // Then + assertThat(result).isNull() + mockLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + BatchFileOrchestrator.ERROR_NOT_BATCH_FILE.format(Locale.US, fakeFile.path) + ) + } + + // endregion + + companion object { + + private const val RECENT_DELAY_MS = 250L + + private const val MAX_ITEM_PER_BATCH: Int = 32 + private const val MAX_ITEM_SIZE: Int = 256 + private const val MAX_BATCH_SIZE: Int = MAX_ITEM_PER_BATCH * (MAX_ITEM_SIZE + 1) + private const val SMALL_ITEM_SIZE: Int = 32 + + private const val OLD_FILE_THRESHOLD: Long = RECENT_DELAY_MS * 4 + private const val MAX_DISK_SPACE = MAX_BATCH_SIZE * 4 + + private const val CLEANUP_FREQUENCY_THRESHOLD_MS = 50L + + private val TEST_PERSISTENCE_CONFIG = FilePersistenceConfig( + RECENT_DELAY_MS, + MAX_BATCH_SIZE.toLong(), + MAX_ITEM_SIZE.toLong(), + MAX_ITEM_PER_BATCH, + OLD_FILE_THRESHOLD, + MAX_DISK_SPACE.toLong(), + CLEANUP_FREQUENCY_THRESHOLD_MS + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriterTest.kt new file mode 100644 index 0000000000..7007439aad --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import com.datadog.android.api.InternalLogger +import com.datadog.android.security.Encryption +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class BatchFileReaderWriterTest { + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Test + fun `M create BatchFileReaderWriter W create() { without encryption }`() { + // When + val readerWriter = BatchFileReaderWriter.create(mockInternalLogger, null) + // Then + assertThat(readerWriter) + .isInstanceOf(PlainBatchFileReaderWriter::class.java) + } + + @Test + fun `M create BatchFileReaderWriter W create() { with encryption }`() { + // When + val mockEncryption = mock() + val readerWriter = BatchFileReaderWriter.create( + mockInternalLogger, + mockEncryption + ) + + // Then + assertThat(readerWriter) + .isInstanceOf(EncryptedBatchReaderWriter::class.java) + + (readerWriter as EncryptedBatchReaderWriter).let { + assertThat(it.delegate).isInstanceOf(PlainBatchFileReaderWriter::class.java) + assertThat(it.encryption).isEqualTo(mockEncryption) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/EncryptedBatchReaderWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/EncryptedBatchReaderWriterTest.kt new file mode 100644 index 0000000000..c54c63c79f --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/EncryptedBatchReaderWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.security.Encryption +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import kotlin.experimental.inv + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class EncryptedBatchReaderWriterTest { + + @Mock + lateinit var mockEncryption: Encryption + + @Mock + lateinit var mockBatchFileReaderWriter: BatchFileReaderWriter + + @Mock + lateinit var mockFile: File + + @Mock + lateinit var mockInternalLogger: InternalLogger + + private lateinit var testedReaderWriter: EncryptedBatchReaderWriter + + @BeforeEach + fun setUp() { + whenever(mockBatchFileReaderWriter.writeData(any(), any(), any())) doReturn true + + whenever(mockEncryption.encrypt(any())) doAnswer { + val bytes = it.getArgument(0) + encrypt(bytes) + } + whenever(mockEncryption.decrypt(any())) doAnswer { + val bytes = it.getArgument(0) + decrypt(bytes) + } + + testedReaderWriter = + EncryptedBatchReaderWriter( + mockEncryption, + mockBatchFileReaderWriter, + mockInternalLogger + ) + } + + // region BatchFileReaderWriter#writeData tests + + @Test + fun `M encrypt data and return true W writeData()`( + @Forgery batchEvent: RawBatchEvent, + @BoolForgery append: Boolean + ) { + // When + val result = testedReaderWriter.writeData( + mockFile, + batchEvent, + append = append + ) + val encryptedData = encrypt(batchEvent.data) + val encryptedMetadata = encrypt(batchEvent.metadata) + + // Then + assertThat(result).isTrue() + verify(mockBatchFileReaderWriter) + .writeData( + mockFile, + RawBatchEvent(data = encryptedData, metadata = encryptedMetadata), + append + ) + + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log internal error and return false W writeData() { bad encryption result }`( + @Forgery batchEvent: RawBatchEvent, + @BoolForgery append: Boolean + ) { + // Given + whenever(mockEncryption.encrypt(batchEvent.data)) doReturn ByteArray(0) + + // When + val result = testedReaderWriter.writeData( + mockFile, + batchEvent, + append = append + ) + + // Then + assertThat(result).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + EncryptedBatchReaderWriter.BAD_ENCRYPTION_RESULT_MESSAGE + ) + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockBatchFileReaderWriter) + } + + // endregion + + // region BatchFileReader#readData tests + + @Test + fun `M decrypt data W readData()`( + @Forgery events: List + ) { + // Given + whenever( + mockBatchFileReaderWriter.readData(mockFile) + ) doReturn events.map { RawBatchEvent(encrypt(it.data), encrypt(it.metadata)) } + + // When + val result = testedReaderWriter.readData(mockFile) + + // Then + assertThat(result).containsExactlyElementsOf(events) + } + + // endregion + + // region writeData + readData + + @Test + fun `M return valid data W writeData() + readData()`( + @Forgery events: List + ) { + // Given + val storage = mutableListOf() + + whenever( + mockBatchFileReaderWriter.writeData( + eq(mockFile), + any(), + eq(true) + ) + ) doAnswer { + storage.add(it.getArgument(1)) + true + } + + whenever( + mockBatchFileReaderWriter.readData(mockFile) + ) doAnswer { storage } + + // When + var writeResult = true + events.forEach { + writeResult = writeResult && testedReaderWriter.writeData(mockFile, it, true) + } + val readResult = testedReaderWriter.readData(mockFile) + + // Then + assertThat(writeResult).isTrue() + assertThat(readResult).containsExactlyElementsOf(events) + + verifyNoInteractions(mockInternalLogger) + } + + // endregion + + // region private + + // this is valid encryption-decryption pair, after the round we will get the original data + private fun encrypt(data: ByteArray): ByteArray { + return data.map { it.inv() }.toByteArray() + } + + private fun decrypt(data: ByteArray): ByteArray { + return data.map { it.inv() }.toByteArray() + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/PlainBatchFileReaderWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/PlainBatchFileReaderWriterTest.kt new file mode 100644 index 0000000000..101861e31e --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/PlainBatchFileReaderWriterTest.kt @@ -0,0 +1,497 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.batch + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.io.File +import java.io.FileNotFoundException +import java.nio.ByteBuffer +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class PlainBatchFileReaderWriterTest { + + private lateinit var testedReaderWriter: PlainBatchFileReaderWriter + + @StringForgery(regex = "([a-z]+)-([a-z]+)") + lateinit var fakeSrcDirName: String + + @StringForgery(regex = "([a-z]+)-([a-z]+)") + lateinit var fakeDstDirName: String + + @TempDir + lateinit var fakeRootDirectory: File + + @Mock + lateinit var mockInternalLogger: InternalLogger + + private lateinit var fakeSrcDir: File + private lateinit var fakeDstDir: File + + @BeforeEach + fun `set up`() { + fakeSrcDir = File(fakeRootDirectory, fakeSrcDirName) + fakeDstDir = File(fakeRootDirectory, fakeDstDirName) + testedReaderWriter = PlainBatchFileReaderWriter(mockInternalLogger) + } + + // region writeData + + @Test + fun `M write data in empty file W writeData() {append=false}`( + @StringForgery fileName: String, + @Forgery event: RawBatchEvent + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.createNewFile() + + // When + val result = testedReaderWriter.writeData( + file, + event, + append = false + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists().hasBinaryContent(encode(event)) + } + + @Test + fun `M write data in empty file W writeData() {append=true}`( + @StringForgery fileName: String, + @Forgery event: RawBatchEvent + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.createNewFile() + + // When + val result = testedReaderWriter.writeData( + file, + event, + append = false + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists().hasBinaryContent(encode(event)) + } + + @Test + fun `M overwrite data in non empty file W writeData() {append=false}`( + @StringForgery fileName: String, + @StringForgery previousContent: String, + @Forgery event: RawBatchEvent + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.writeText(previousContent) + + // When + val result = testedReaderWriter.writeData( + file, + event, + append = false + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists().hasBinaryContent(encode(event)) + } + + @Test + fun `M append data in non empty file W writeData() {append=true}`( + @StringForgery fileName: String, + @Forgery previousEvent: RawBatchEvent, + @Forgery event: RawBatchEvent + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.writeBytes(encode(previousEvent)) + + // When + val result = testedReaderWriter.writeData( + file, + event, + append = true + ) + + // Then + assertThat(result).isTrue() + assertThat(file).exists() + .hasBinaryContent( + encode(previousEvent) + encode(event) + ) + } + + @Test + fun `M return false and warn W writeData() {parent dir does not exist}`( + @StringForgery fileName: String, + @Forgery event: RawBatchEvent, + @BoolForgery append: Boolean + ) { + // Given + assumeFalse(fakeSrcDir.exists()) + val file = File(fakeSrcDir, fileName) + + // When + val result = testedReaderWriter.writeData( + file, + event, + append = append + ) + + // Then + assertThat(result).isFalse() + assertThat(file).doesNotExist() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER), + PlainBatchFileReaderWriter.ERROR_WRITE.format(Locale.US, file.path), + FileNotFoundException::class.java + ) + } + + @Test + fun `M return false and warn W writeData() {file is not file}`( + @StringForgery fileName: String, + @Forgery event: RawBatchEvent, + @BoolForgery append: Boolean + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.mkdirs() + + // When + val result = testedReaderWriter.writeData( + file, + event, + append = append + ) + + // Then + assertThat(result).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER), + PlainBatchFileReaderWriter.ERROR_WRITE.format(Locale.US, file.path), + FileNotFoundException::class.java + ) + } + + // endregion + + // region readData + + @Test + fun `M return empty list and warn W readData() {file does not exist}`( + @StringForgery fileName: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + assumeFalse(file.exists()) + + // When + val result = testedReaderWriter.readData(file) + + // Then + assertThat(result).isEmpty() + assertThat(file).doesNotExist() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + PlainBatchFileReaderWriter.ERROR_READ.format(Locale.US, file.path), + FileNotFoundException::class.java + ) + } + + @Test + fun `M return empty list and warn W readData() {file is not file}`( + @StringForgery fileName: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + assumeFalse(file.exists()) + + // When + val result = testedReaderWriter.readData(file) + + // Then + assertThat(result).isEmpty() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + PlainBatchFileReaderWriter.ERROR_READ.format(Locale.US, file.path), + FileNotFoundException::class.java + ) + } + + @Test + fun `M return empty list and warn user W readData() { corrupted data }`( + @StringForgery fileName: String, + @StringForgery content: String + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.writeBytes(content.toByteArray()) + + // When + val result = testedReaderWriter.readData(file) + + // Then + assertThat(result).isEmpty() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + PlainBatchFileReaderWriter.WARNING_NOT_ALL_DATA_READ.format(Locale.US, file.path) + ) + } + + @Test + fun `M return valid events read so far and warn W readData() { stream cutoff }`( + @StringForgery fileName: String, + forge: Forge + ) { + // Given + val file = File(fakeRootDirectory, fileName) + val events = forge.aList { + RawBatchEvent(aString().toByteArray()) + } + + file.writeBytes( + events.mapIndexed { index, event -> + if (index == events.lastIndex) { + encode(event) + .let { it.take(forge.anInt(min = 1, max = it.size - 1)) } + .toByteArray() + } else { + encode(event) + } + }.reduce { acc, bytes -> acc + bytes } + ) + + // When + val result = testedReaderWriter.readData(file) + + // Then + assertThat(result).containsExactlyElementsOf(events.take(events.size - 1)) + } + + @Test + fun `M return valid events read so far and warn W readData() { unexpected block type }`( + @StringForgery fileName: String, + @Forgery events: List, + forge: Forge + ) { + // Given + val file = File(fakeRootDirectory, fileName) + + val badEventIndex = forge.anInt(min = 0, max = events.size) + file.writeBytes( + events.mapIndexed { index, item -> + val metaBytes = metaBytesAsTlv(item.metadata) + val eventBytes = dataBytesAsTlv(item.data) + if (index == badEventIndex) { + val isBadBlockTypeInMeta = forge.aBool() + if (isBadBlockTypeInMeta) { + metaBytes.apply { + set( + 1, + // first 2 bytes of meta should be 1, so to generate + // wrong block we need any value != 1 + forge.anElementFrom( + 0, + forge.anInt(min = 2, max = Byte.MAX_VALUE + 1) + ).toByte() + ) + } + eventBytes + } else { + // first 2 bytes of event should be 0, so to generate + // wrong block we need any value != 0 + metaBytes + eventBytes.apply { + set(1, forge.anInt(min = 1, max = Byte.MAX_VALUE + 1).toByte()) + } + } + } else { + metaBytes + eventBytes + } + }.reduce { acc, bytes -> acc + bytes } + ) + + // When + val result = testedReaderWriter.readData(file) + + // Then + assertThat(result).containsExactlyElementsOf(events.take(badEventIndex)) + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + PlainBatchFileReaderWriter.WARNING_NOT_ALL_DATA_READ.format(Locale.US, file.path) + ) + } + + @Test + fun `M return file content W readData() { single event }`( + @StringForgery fileName: String, + @Forgery event: RawBatchEvent + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.writeBytes(encode(event)) + + // When + val result = testedReaderWriter.readData(file) + + // Then + assertThat(result).containsExactlyElementsOf(listOf(event)) + } + + @Test + fun `M return file content W readData() { multiple events }`( + @StringForgery fileName: String, + @Forgery events: List + ) { + // Given + val file = File(fakeRootDirectory, fileName) + file.writeBytes(events.map { encode(it) }.reduce { acc, bytes -> acc + bytes }) + + // When + val result = testedReaderWriter.readData(file) + + // Then + assertThat(result).containsExactlyElementsOf(events) + } + + // endregion + + // region writeData + readData + + @Test + fun `M return file content W writeData + readData() { append = false }`( + @StringForgery fileName: String, + @Forgery event: RawBatchEvent + ) { + // Given + val file = File(fakeRootDirectory, fileName) + + // When + val writeResult = testedReaderWriter.writeData(file, event, false) + val readResult = testedReaderWriter.readData(file) + + // Then + assertThat(writeResult).isTrue() + assertThat(readResult).containsExactlyElementsOf(listOf(event)) + } + + @Test + fun `M return file content W writeData + readData() { append = true }`( + @StringForgery fileName: String, + @Forgery events: List + ) { + // Given + val file = File(fakeRootDirectory, fileName) + + // When + var writeResult = true + events.forEach { + writeResult = writeResult && testedReaderWriter.writeData( + file, + it, + true + ) + } + val readResult = testedReaderWriter.readData(file) + + // Then + assertThat(writeResult).isTrue() + assertThat(readResult).containsExactlyElementsOf(events) + } + + // endregion + + // region Reading older formats + + @Test + fun `M return file content W readData() { 2-2-0 and earlier }`() { + // 2.3.0 release is changing the way we are handling metadata, so we need to make sure + // that we are backward compatible with data written earlier + + // Given + val file = File( + checkNotNull(javaClass.classLoader) + .getResource("logs-batch-2.2.0-and-earlier") + .file + ) + + // When + val readResult = testedReaderWriter.readData(file) + + // Then + assertThat(readResult).hasSize(2) + assertThat(readResult).satisfies { it.all { it.data.isNotEmpty() } } + assertThat(readResult).satisfies { it.all { it.metadata.isEmpty() } } + } + + // endregion + + // region private + + // Encoding specification is as following: + // +- 2 bytes -+- 4 bytes -+- n bytes -| + // | block type | data size (n) | data | + // +------------+---------------+-----------+ + // where block type is 0x00 for event, 0x01 for data + private fun encode(event: RawBatchEvent): ByteArray { + return metaBytesAsTlv(event.metadata) + dataBytesAsTlv(event.data) + } + + private fun metaBytesAsTlv(meta: ByteArray): ByteArray { + return ByteBuffer.allocate(6 + meta.size) + .putShort(0x01) + .putInt(meta.size) + .put(meta) + .array() + } + + private fun dataBytesAsTlv(data: ByteArray): ByteArray { + return ByteBuffer.allocate(6 + data.size) + .putShort(0x00) + .putInt(data.size) + .put(data) + .array() + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleItemDataWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleItemDataWriterTest.kt new file mode 100644 index 0000000000..54b3e8ec55 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/single/SingleItemDataWriterTest.kt @@ -0,0 +1,196 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.single + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.DataWriter +import com.datadog.android.core.internal.persistence.file.FileOrchestrator +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.FileWriter +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import org.mockito.stubbing.Answer +import java.io.File +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class SingleItemDataWriterTest { + + lateinit var testedWriter: DataWriter + + @Mock + lateinit var mockSerializer: Serializer + + @Mock + lateinit var mockOrchestrator: FileOrchestrator + + @Mock + lateinit var mockFileWriter: FileWriter + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Forgery + lateinit var fakeThrowable: Throwable + + @Forgery + lateinit var fakeFilePersistenceConfig: FilePersistenceConfig + + private val stubReverseSerializerAnswer = Answer { invocation -> + (invocation.getArgument(0)).reversed() + } + + private val stubFailingSerializerAnswer = Answer { null } + + private val stubThrowingSerializerAnswer = Answer { + throw fakeThrowable + } + + @BeforeEach + fun `set up`() { + whenever(mockSerializer.serialize(any())).doAnswer(stubReverseSerializerAnswer) + + testedWriter = SingleItemDataWriter( + mockOrchestrator, + mockSerializer, + mockFileWriter, + mockInternalLogger, + fakeFilePersistenceConfig.copy(maxItemSize = Long.MAX_VALUE) + ) + } + + @Test + fun `M write element to file W write(element)`( + @StringForgery data: String, + @Forgery file: File + ) { + // Given + val serialized = data.reversed().toByteArray(Charsets.UTF_8) + whenever(mockOrchestrator.getWritableFile()) doReturn file + + // When + testedWriter.write(data) + + // Then + verify(mockFileWriter) + .writeData( + file, + serialized, + append = false + ) + } + + @Test + fun `M write last element to file W write(list)`( + @StringForgery data: List, + @Forgery file: File + ) { + // Given + val lastSerialized = data.last().reversed().toByteArray(Charsets.UTF_8) + whenever(mockOrchestrator.getWritableFile()) doReturn file + + // When + testedWriter.write(data) + + // Then + verify(mockFileWriter) + .writeData( + file, + lastSerialized, + append = false + ) + } + + @Test + fun `M do nothing W write(element) { serialization to null }`( + @StringForgery data: String + ) { + // Given + whenever(mockSerializer.serialize(data)) doAnswer stubFailingSerializerAnswer + + // When + testedWriter.write(data) + + // Then + verifyNoInteractions(mockFileWriter) + } + + @Test + fun `M do nothing W write(element) { serialization exception }`( + @StringForgery data: String + ) { + // Given + whenever(mockSerializer.serialize(data)) doAnswer stubThrowingSerializerAnswer + + // When + testedWriter.write(data) + + // Then + verifyNoInteractions(mockFileWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + Serializer.ERROR_SERIALIZING.format(Locale.US, data.javaClass.simpleName), + fakeThrowable + ) + } + + @Test + fun `M do nothing W write(element) { element is too big }`( + @StringForgery data: String + ) { + // When + val dataSize = data.toByteArray(Charsets.UTF_8).size + val maxLimit = (dataSize - 1).toLong() + + // Given + testedWriter = SingleItemDataWriter( + mockOrchestrator, + mockSerializer, + mockFileWriter, + mockInternalLogger, + fakeFilePersistenceConfig.copy( + maxItemSize = maxLimit + ) + ) + + // When + testedWriter.write(data) + + // Then + verifyNoInteractions(mockFileWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + SingleItemDataWriter.ERROR_LARGE_DATA.format(Locale.US, dataSize, maxLimit) + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt new file mode 100644 index 0000000000..eb72af2bba --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt @@ -0,0 +1,193 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader.Companion.FAILED_TO_DESERIALIZE_ERROR +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.nio.ByteBuffer + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class TLVBlockFileReaderTest { + private lateinit var testedReader: TLVBlockFileReader + + @Mock + private lateinit var mockFile: File + + @Mock + private lateinit var mockFileReaderWriter: FileReaderWriter + + @Mock + private lateinit var mockInternalLogger: InternalLogger + + @StringForgery + private lateinit var fakeDataString: String + + private lateinit var fakeVersionBytes: ByteArray + private lateinit var fakeDataBytes: ByteArray + private lateinit var fakeBufferBytes: ByteArray + + @BeforeEach + fun setup(@IntForgery(min = 0) fakeVersion: Int) { + val versionBytes = createVersionBytes(fakeVersion) + val dataBytes = createDataBytes() + val dataToWrite = versionBytes + dataBytes + + whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(dataToWrite) + + testedReader = TLVBlockFileReader( + fileReaderWriter = mockFileReaderWriter, + internalLogger = mockInternalLogger + ) + } + + @Test + fun `M return empty collection W read() { invalid TLV type }`() { + // Given + fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) + whenever(mockFileReaderWriter.readData(mockFile)) + .thenReturn(fakeBufferBytes) + + // When + val readBytes = testedReader.read(file = mockFile) + + // Then + assertThat(readBytes).isEmpty() + } + + @Test + fun `M log error W read() { invalid TLV type }`() { + // Given + fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) + whenever(mockFileReaderWriter.readData(mockFile)) + .thenReturn(fakeBufferBytes) + + // When + testedReader.read(file = mockFile) + + // Then + val expectedMessage = if (fakeDataString.length >= 2) { + "TLV header corrupt. Invalid type" + } else { + "Failed to deserialize TLV data length" + } + val captor = argumentCaptor<() -> String>() + verify(mockInternalLogger).log( + level = eq(InternalLogger.Level.WARN), + target = eq(InternalLogger.Target.MAINTAINER), + captor.capture(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + assertThat(captor.firstValue.invoke()) + .startsWith(expectedMessage) + } + + @Test + fun `M return valid object W read() { valid TLV format }`() { + // When + val tlvArray = testedReader.read(file = mockFile) + + // Then + assertThat(tlvArray).hasSize(2) + val versionObject = tlvArray[0] + val dataObject = tlvArray[1] + + assertThat(versionObject.type).isEqualTo(TLVBlockType.VERSION_CODE) + assertThat(versionObject.data).isEqualTo(fakeVersionBytes) + assertThat(dataObject.type).isEqualTo(TLVBlockType.DATA) + assertThat(dataObject.data).isEqualTo(fakeDataBytes) + } + + @Test + fun `M return empty array W read() { invalid type length }`() { + // Given + val fakeByteArray = ByteBuffer.allocate(1).array() + whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(fakeByteArray) + + // When + val result = testedReader.read(mockFile) + + // Then + assertThat(result).isEmpty() + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + message = FAILED_TO_DESERIALIZE_ERROR + ) + } + + @Test + fun `M return empty array W read() { invalid data length }`() { + // Given + val fakeBuffer = ByteBuffer.allocate(3) + val fakeArray = fakeBuffer.putShort(TLVBlockType.DATA.rawValue.toShort()).array() + whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(fakeArray) + + // When + val result = testedReader.read(mockFile) + + // Then + assertThat(result).isEmpty() + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + message = FAILED_TO_DESERIALIZE_ERROR + ) + } + + private fun createVersionBytes(fakeVersion: Int): ByteArray { + val versionType = TLVBlockType.VERSION_CODE.rawValue.toShort() + fakeVersionBytes = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(fakeVersion).array() + + return ByteBuffer + .allocate(fakeVersionBytes.size + Int.SIZE_BYTES + Short.SIZE_BYTES) + .putShort(versionType) + .putInt(fakeVersionBytes.size) + .put(fakeVersionBytes) + .array() + } + + private fun createDataBytes(): ByteArray { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + val dataType = TLVBlockType.DATA.rawValue.toShort() + + return ByteBuffer + .allocate(fakeDataBytes.size + Int.SIZE_BYTES + Short.SIZE_BYTES) + .putShort(dataType) + .putInt(fakeDataBytes.size) + .put(fakeDataBytes) + .array() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt new file mode 100644 index 0000000000..a174e71bb1 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.nio.ByteBuffer + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class TLVBlockTest { + private lateinit var testedTLVBlock: TLVBlock + + @Mock + lateinit var mockTLVBlockType: TLVBlockType + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Test + fun `M return null W serialize() { empty data }`() { + // Given + testedTLVBlock = TLVBlock( + type = mockTLVBlockType, + data = ByteArray(0), + internalLogger = mockInternalLogger + ) + + // When + val block = testedTLVBlock.serialize() + + // Then + assertThat(block).isNull() + } + + @Test + fun `M use appropriate TLV datatypes W serialize() { has data }`( + @StringForgery fakeString: String, + @IntForgery(min = 0, max = 10) fakeTypeAsInt: Int + ) { + // Given + val fakeTLVType = fakeTypeAsInt.toShort() + val fakeByteArray = fakeString.toByteArray(Charsets.UTF_8) + whenever(mockTLVBlockType.rawValue).thenReturn(fakeTLVType.toUShort()) + + testedTLVBlock = TLVBlock( + type = mockTLVBlockType, + data = fakeByteArray, + internalLogger = mockInternalLogger + ) + + // When + val block = testedTLVBlock.serialize() + + // Then + checkNotNull(block) + assertThat(block.size).isEqualTo(fakeString.length + 6) + val type = block.copyOfRange(0, 2) + val length = block.copyOfRange(2, 6) + val data = block.copyOfRange(6, block.size) + val typeAsShort = type.let { ByteBuffer.wrap(it).getShort() } + val lengthAsInt = length.let { ByteBuffer.wrap(it).getInt() } + + assertThat(typeAsShort).isEqualTo(fakeTLVType) + assertThat(lengthAsInt).isEqualTo(data.size) + } + + @Test + fun `M log error W serialize() { exceeds max entry size }`( + @StringForgery fakeString: String, + @IntForgery(min = 0, max = 10) fakeTypeAsInt: Int + ) { + // Given + val fakeTLVType = fakeTypeAsInt.toShort() + val fakeByteArray = fakeString.toByteArray(Charsets.UTF_8) + whenever(mockTLVBlockType.rawValue).thenReturn(fakeTLVType.toUShort()) + + testedTLVBlock = TLVBlock( + type = mockTLVBlockType, + data = fakeByteArray, + internalLogger = mockInternalLogger + ) + + // When + testedTLVBlock.serialize(1) + + // Then + val stringCaptor = argumentCaptor<() -> String>() + verify(mockInternalLogger).log( + level = eq(InternalLogger.Level.WARN), + target = eq(InternalLogger.Target.MAINTAINER), + stringCaptor.capture(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + assertThat(stringCaptor.firstValue.invoke()).startsWith("DataBlock length exceeds limit") + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt new file mode 100644 index 0000000000..acf7ff5c31 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class TLVBlockTypeTest { + @Test + fun `M return type value W fromValue() { existing value }`() { + // When + val shortValue = TLVBlockType.fromValue(TLVBlockType.VERSION_CODE.rawValue) + + // Then + assertThat(shortValue).isEqualTo(TLVBlockType.VERSION_CODE) + } + + @Test + fun `M return null W fromValue() { nonexistent value }`() { + // When + val shortValue = TLVBlockType.fromValue(999u) + + // Then + assertThat(shortValue).isNull() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiverTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiverTest.kt new file mode 100644 index 0000000000..6bac8f7b08 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiverTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.receiver + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.tools.unit.extensions.ApiLevelExtension +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ThreadSafeReceiverTest { + private lateinit var testedReceiver: ThreadSafeReceiver + + @Mock + lateinit var mockContext: Context + + @Mock + lateinit var mockIntentFilter: IntentFilter + + @BeforeEach + fun `set up`() { + testedReceiver = TestableThreadSafeReceiver() + } + + // region registerReceiver + + @TestTargetApi(Build.VERSION_CODES.O) + @Test + fun `M use the no export flag W registerReceiver { API version above 26}`() { + // When + testedReceiver.registerReceiver(mockContext, mockIntentFilter) + + // Then + verify(mockContext).registerReceiver( + testedReceiver, + mockIntentFilter, + ThreadSafeReceiver.RECEIVER_NOT_EXPORTED_COMPAT + ) + assertThat(this.testedReceiver.isRegistered.get()).isTrue() + } + + @TestTargetApi(Build.VERSION_CODES.TIRAMISU) + @Test + fun `M use the no export flag W registerReceiver { API version above 33}`() { + // When + testedReceiver.registerReceiver(mockContext, mockIntentFilter) + + // Then + verify(mockContext).registerReceiver( + testedReceiver, + mockIntentFilter, + Context.RECEIVER_NOT_EXPORTED + ) + assertThat(this.testedReceiver.isRegistered.get()).isTrue() + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + @TestTargetApi(Build.VERSION_CODES.N) + @Test + fun `M not use the no export flag W registerReceiver { API version below 26}`() { + // When + testedReceiver.registerReceiver(mockContext, mockIntentFilter) + + // Then + verify(mockContext).registerReceiver(testedReceiver, mockIntentFilter) + verifyNoMoreInteractions(mockContext) + assertThat(this.testedReceiver.isRegistered.get()).isTrue() + } + + // endregion + + // region unregisterReceiver + + @Test + fun `M unregister the receiver W unregisterReceiver { registered }`() { + // Given + testedReceiver.isRegistered.set(true) + + // When + testedReceiver.unregisterReceiver(mockContext) + + // Then + verify(mockContext).unregisterReceiver(testedReceiver) + assertThat(this.testedReceiver.isRegistered.get()).isFalse() + } + + @Test + fun `M do nothing W unregisterReceiver { not registered }`() { + // Given + testedReceiver.isRegistered.set(false) + + // When + testedReceiver.unregisterReceiver(mockContext) + + // Then + verifyNoInteractions(mockContext) + assertThat(this.testedReceiver.isRegistered.get()).isFalse() + } + + // endregion +} + +internal class TestableThreadSafeReceiver : ThreadSafeReceiver() { + override fun onReceive(context: Context?, intent: Intent?) {} +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProviderTest.kt new file mode 100644 index 0000000000..6e12ff47df --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProviderTest.kt @@ -0,0 +1,491 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import android.content.Context +import android.content.Intent +import android.os.BatteryManager +import android.os.PowerManager +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.assertj.SystemInfoAssert.Companion.assertThat +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.same +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.math.roundToInt + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(value = Configurator::class) +internal class BroadcastReceiverSystemInfoProviderTest { + + private lateinit var testedProvider: BroadcastReceiverSystemInfoProvider + + @Mock + lateinit var mockContext: Context + + @Mock + lateinit var mockIntent: Intent + + @Mock + lateinit var mockPowerMgr: PowerManager + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @IntForgery + var fakePluggedStatus: Int = 0 + + @BeforeEach + fun `set up`() { + whenever(mockContext.getSystemService(Context.POWER_SERVICE)) doReturn mockPowerMgr + + testedProvider = + BroadcastReceiverSystemInfoProvider(mockInternalLogger) + } + + @Test + fun `M ignore W unregister() {register not called}`() { + // When + testedProvider.unregister(mockContext) + + // Then + verifyNoInteractions(mockContext) + } + + @Test + fun `M unregister only once W unregister()+unregister()`() { + // Given + val countDownLatch = CountDownLatch(2) + testedProvider.register(mockContext) + + // When + Thread { + testedProvider.unregister(mockContext) + countDownLatch.countDown() + }.start() + Thread { + testedProvider.unregister(mockContext) + countDownLatch.countDown() + }.start() + + // Then + countDownLatch.await(3, TimeUnit.SECONDS) + verify(mockContext).unregisterReceiver(testedProvider) + } + + @Test + fun `M return unknown W getLatestSystemInfo() {not registered}`() { + // When + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasBatteryLevel(-1) + .hasBatteryFullOrCharging(false) + .hasPowerSaveMode(false) + .hasOnExternalPowerSource(false) + } + + @RepeatedTest(10) + fun `M read system info W register()`( + @Forgery status: SystemInfo.BatteryStatus, + @IntForgery(min = 0, max = 100) level: Int, + @IntForgery(min = 50, max = 10000) scale: Int, + @BoolForgery powerSaveMode: Boolean + ) { + // Given + val batteryIntent: Intent = mock() + val scaledLevel = ((level * scale) / 100f).roundToInt() + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_STATUS), any())) + .doReturn(status.androidStatus()) + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_PLUGGED), any())) + .doReturn(fakePluggedStatus) + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) + .doReturn(scaledLevel) + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale + whenever(batteryIntent.getBooleanExtra(eq(BatteryManager.EXTRA_PRESENT), any())) + .doReturn(true) + whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + val powerSaveModeIntent: Intent = mock() + whenever(mockPowerMgr.isPowerSaveMode) doReturn powerSaveMode + whenever(powerSaveModeIntent.action) doReturn PowerManager.ACTION_POWER_SAVE_MODE_CHANGED + doReturn(batteryIntent, powerSaveModeIntent) + .whenever(mockContext).registerReceiver(same(testedProvider), any()) + + // When + testedProvider.register(mockContext) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasBatteryLevel(level, scale) + .hasPowerSaveMode(powerSaveMode) + .hasOnExternalPowerSource(false) + } + + @Test + fun `M update data W onReceive() {battery changed, null value}`() { + // Given + whenever(mockIntent.getIntExtra(any(), any())) doAnswer { + it.arguments[1] as Int + } + whenever(mockIntent.getBooleanExtra(any(), any())) doAnswer { + it.arguments[1] as Boolean + } + whenever(mockIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, mockIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasBatteryLevel(-1) + .hasBatteryFullOrCharging(false) + .hasOnExternalPowerSource(false) + .hasPowerSaveMode(false) + } + + @Test + fun `M update data W onReceive() {battery changed, not null value}`( + @Forgery status: SystemInfo.BatteryStatus, + @IntForgery(min = 0, max = 100) level: Int, + @IntForgery(min = 50, max = 10000) scale: Int + ) { + // Given + val scaledLevel = ((level * scale) / 100f).roundToInt() + whenever(mockIntent.getIntExtra(eq(BatteryManager.EXTRA_STATUS), any())) + .doReturn(status.androidStatus()) + whenever(mockIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn scaledLevel + whenever(mockIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale + whenever(mockIntent.getBooleanExtra(eq(BatteryManager.EXTRA_PRESENT), any())) + .doReturn(true) + whenever(mockIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, mockIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasBatteryLevel(level, scale) + .hasOnExternalPowerSource(false) + .hasPowerSaveMode(false) + } + + @Test + fun `M update data W onReceive() {power save changed}`( + @BoolForgery powerSaveMode: Boolean + ) { + // Given + + whenever(mockPowerMgr.isPowerSaveMode) doReturn powerSaveMode + whenever(mockIntent.action) doReturn PowerManager.ACTION_POWER_SAVE_MODE_CHANGED + + // When + testedProvider.onReceive(mockContext, mockIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasPowerSaveMode(powerSaveMode) + } + + @Test + fun `M update data W onReceive() {power save changed, no PowerManager}`() { + // Given + + whenever(mockContext.getSystemService(Context.POWER_SERVICE)) doReturn null + whenever(mockIntent.action) doReturn PowerManager.ACTION_POWER_SAVE_MODE_CHANGED + + // When + testedProvider.onReceive(mockContext, mockIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasPowerSaveMode(false) + } + + @Test + fun `M ignore W onReceive() {unknown action}`( + @StringForgery action: String + ) { + // Given + whenever(mockIntent.action) doReturn action + + // When + testedProvider.onReceive(mockContext, mockIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasBatteryLevel(-1) + .hasBatteryFullOrCharging(false) + .hasPowerSaveMode(false) + .hasOnExternalPowerSource(false) + } + + @Test + fun `M ignore W onReceive() {null intent}`() { + // When + testedProvider.onReceive(mockContext, null) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo) + .hasBatteryLevel(-1) + .hasBatteryFullOrCharging(false) + .hasPowerSaveMode(false) + .hasOnExternalPowerSource(false) + } + + @ParameterizedTest + @ValueSource( + ints = [ + BatteryManager.BATTERY_STATUS_DISCHARGING, + BatteryManager.BATTERY_STATUS_UNKNOWN, + BatteryManager.BATTERY_STATUS_NOT_CHARGING + ] + ) + fun `M set batteryFullOrCharging to false W onReceive { battery status not charging or full }`( + status: Int, + @IntForgery(min = 0, max = 100) level: Int, + @IntForgery(min = 50, max = 10000) scale: Int + ) { + // Given + val scaledLevel = ((level * scale) / 100f).roundToInt() + val batteryIntent: Intent = mock() + whenever( + batteryIntent.getIntExtra( + BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN + ) + ).thenReturn(status) + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn + scaledLevel + whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, batteryIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo).hasBatteryFullOrCharging(false) + } + + @ParameterizedTest + @ValueSource( + ints = [ + BatteryManager.BATTERY_STATUS_CHARGING, + BatteryManager.BATTERY_STATUS_FULL + ] + ) + fun `M set batteryFullOrCharging to true W onReceive { battery status charging or full }`( + status: Int, + @IntForgery(min = 0, max = 100) level: Int, + @IntForgery(min = 50, max = 10000) scale: Int + ) { + // Given + val scaledLevel = ((level * scale) / 100f).roundToInt() + val batteryIntent: Intent = mock() + whenever( + batteryIntent.getIntExtra( + BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN + ) + ).thenReturn(status) + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn + scaledLevel + whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, batteryIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo).hasBatteryFullOrCharging(true) + } + + @ParameterizedTest + @ValueSource( + ints = [ + BatteryManager.BATTERY_PLUGGED_AC, + BatteryManager.BATTERY_PLUGGED_WIRELESS, + BatteryManager.BATTERY_PLUGGED_USB + ] + ) + fun `M set onExternalPowerSource to true W onReceive { on external power source }`( + pluggedInStatus: Int, + @IntForgery( + min = 0, + max = 100 + ) level: Int, + @IntForgery(min = 50, max = 10000) scale: Int + ) { + // Given + val scaledLevel = ((level * scale) / 100f).roundToInt() + val batteryIntent: Intent = mock() + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn + scaledLevel + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_PLUGGED), any())) + .doReturn(pluggedInStatus) + whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, batteryIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo).hasOnExternalPowerSource(true) + } + + @Test + fun `M set onExternalPowerSource to false W onReceive { not on external power source }`( + @IntForgery( + min = 0, + max = 100 + ) level: Int, + @IntForgery(min = 50, max = 10000) scale: Int + ) { + // Given + val scaledLevel = ((level * scale) / 100f).roundToInt() + val batteryIntent: Intent = mock() + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn + scaledLevel + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_PLUGGED), any())) + .doReturn(0) + whenever(batteryIntent.getBooleanExtra(eq(BatteryManager.EXTRA_PRESENT), any())) + .doReturn(true) + whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, batteryIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo).hasOnExternalPowerSource(false) + } + + @Test + fun `M set onExternalPowerSource to true W onReceive { battery absent }`() { + // Given + val batteryIntent: Intent = mock() + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn 100 + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn 0 + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_PLUGGED), any())) doReturn 0 + whenever(batteryIntent.getBooleanExtra(eq(BatteryManager.EXTRA_PRESENT), any())) + .doReturn(false) + whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, batteryIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo).hasOnExternalPowerSource(true) + } + + @Test + fun `M set onExternalPowerSource to false W onReceive { battery present }`() { + // Given + val batteryIntent: Intent = mock() + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn 100 + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn 0 + whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_PLUGGED), any())) doReturn 0 + whenever(batteryIntent.getBooleanExtra(eq(BatteryManager.EXTRA_PRESENT), any())) + .doReturn(true) + whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED + + // When + testedProvider.onReceive(mockContext, batteryIntent) + val systemInfo = testedProvider.getLatestSystemInfo() + + // Then + assertThat(systemInfo).hasOnExternalPowerSource(false) + } + + @Test + fun `M log error W onReceive { exception during intent processing }`( + forge: Forge + ) { + // Given + val intent = mock() + val intentType = forge.anElementFrom( + Intent.ACTION_BATTERY_CHANGED, + PowerManager.ACTION_POWER_SAVE_MODE_CHANGED + ) + whenever(intent.action) doReturn intentType + when (intentType) { + Intent.ACTION_BATTERY_CHANGED -> { + whenever(intent.getIntExtra(any(), any())) doThrow RuntimeException() + } + PowerManager.ACTION_POWER_SAVE_MODE_CHANGED -> { + val mockPowerManager = mock() + whenever(mockContext.getSystemService(Context.POWER_SERVICE)) doReturn mockPowerManager + whenever(mockPowerManager.isPowerSaveMode) doThrow RuntimeException() + } + } + + // When + Then + assertDoesNotThrow { + testedProvider.onReceive(mockContext, intent) + } + } + + // endregion + + // region Internal + + private fun SystemInfo.BatteryStatus.androidStatus(): Int { + return when (this) { + SystemInfo.BatteryStatus.UNKNOWN -> BatteryManager.BATTERY_STATUS_UNKNOWN + SystemInfo.BatteryStatus.CHARGING -> BatteryManager.BATTERY_STATUS_CHARGING + SystemInfo.BatteryStatus.DISCHARGING -> BatteryManager.BATTERY_STATUS_DISCHARGING + SystemInfo.BatteryStatus.NOT_CHARGING -> BatteryManager.BATTERY_STATUS_NOT_CHARGING + SystemInfo.BatteryStatus.FULL -> BatteryManager.BATTERY_STATUS_FULL + } + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProviderTest.kt new file mode 100644 index 0000000000..07030688ef --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProviderTest.kt @@ -0,0 +1,583 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import android.app.UiModeManager +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.content.res.Resources +import android.hardware.display.DisplayManager +import android.os.Build +import android.os.LocaleList +import android.telephony.TelephonyManager +import android.view.Display +import com.datadog.android.api.context.DeviceType +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.tools.unit.extensions.ApiLevelExtension +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DefaultAndroidInfoProviderTest { + + private lateinit var testedProvider: AndroidInfoProvider + + @Mock + lateinit var mockSdkVersionProvider: BuildSdkVersionProvider + + @Mock + lateinit var mockContext: Context + + @Mock + lateinit var mockUiModeManager: UiModeManager + + @Mock + lateinit var mockTelephonyManager: TelephonyManager + + @Mock + lateinit var mockPackageManager: PackageManager + + @Mock + lateinit var mockResources: Resources + + @Mock + lateinit var mockConfiguration: Configuration + + @StringForgery + lateinit var fakeDeviceBrand: String + + @StringForgery + lateinit var fakeDeviceModel: String + + @StringForgery + lateinit var fakeDeviceId: String + + @StringForgery(regex = "[1-9]{1,3}\\.[1-9]{1,3}\\.[1-9]{1,3}") + lateinit var fakeOsVersion: String + + @BeforeEach + fun setUp(forge: Forge) { + whenever(mockContext.getSystemService(Context.UI_MODE_SERVICE)) doReturn mockUiModeManager + whenever(mockContext.getSystemService(Context.TELEPHONY_SERVICE)) doReturn mockTelephonyManager + whenever(mockSdkVersionProvider.version) doReturn forge.anInt(min = Build.VERSION_CODES.BASE) + whenever(mockContext.packageManager) doReturn mockPackageManager + whenever(mockContext.resources) doReturn mockResources + whenever(mockResources.configuration) doReturn mockConfiguration + + fakeDeviceModel = "" + } + + // region device type + + @Test + fun `M return TV type W deviceType { UI_MODE_TYPE_TELEVISION }`() { + // Given + whenever(mockUiModeManager.currentModeType) doReturn Configuration.UI_MODE_TYPE_TELEVISION + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.TV) + } + + @Test + fun `M return TV type W deviceType { FEATURE_LEANBACK }`() { + // Given + whenever( + mockPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + ) doReturn true + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.TV) + } + + @Test + fun `M return TV type W deviceType { FEATURE_GOOGLE_ANDROID_TV }`() { + // Given + whenever( + mockPackageManager.hasSystemFeature( + DefaultAndroidInfoProvider.FEATURE_GOOGLE_ANDROID_TV + ) + ) doReturn true + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.TV) + } + + @Test + fun `M return Tablet type W deviceType { Samsung SM-T series model }`( + @IntForgery(1) tModelVersion: Int + ) { + // Given + fakeDeviceModel = "SM-T$tModelVersion" + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.TABLET) + } + + @Test + fun `M return Tablet type W deviceType { tablet word in model name }`( + @StringForgery(regex = "[a-zA-Z1-9 ]{0,9}Tablet[a-zA-Z1-9 ]{0,9}") fakeModel: String + ) { + // Given + fakeDeviceModel = fakeModel + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.TABLET) + } + + @Test + fun `M return Tablet type W deviceType { smallest screen width more than 800dp }`( + @IntForgery(min = DefaultAndroidInfoProvider.MIN_TABLET_WIDTH_DP) fakeWidth: Int + ) { + // Given + val mockResources = mock() + val fakeConfiguration = Configuration().apply { + smallestScreenWidthDp = fakeWidth + } + + whenever(mockContext.resources) doReturn mockResources + whenever(mockResources.configuration) doReturn fakeConfiguration + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.TABLET) + } + + @Test + fun `M return Mobile type W deviceType { phone word in model name }`( + @StringForgery(regex = "[a-zA-Z1-9 ]{0,9}Phone[a-zA-Z1-9 ]{0,9}") fakeModel: String + ) { + // Given + fakeDeviceModel = fakeModel + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.MOBILE) + } + + @ParameterizedTest + @MethodSource("phoneTypesWithDescription") + fun `M return Mobile type W deviceType {smallest screen width less than 800dp + telephony}`( + phoneType: PhoneType, + @IntForgery(min = 0, max = DefaultAndroidInfoProvider.MIN_TABLET_WIDTH_DP) fakeWidth: Int + ) { + // Given + val mockResources = mock() + val fakeConfiguration = Configuration().apply { + smallestScreenWidthDp = fakeWidth + } + whenever(mockContext.resources) doReturn mockResources + whenever(mockResources.configuration) doReturn fakeConfiguration + whenever(mockTelephonyManager.phoneType) doReturn phoneType.value + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.MOBILE) + } + + @Test + fun `M return Other type W deviceType { no tv, table or mobile properties }`( + @IntForgery( + min = 0, + max = DefaultAndroidInfoProvider.MIN_TABLET_WIDTH_DP + ) fakeWidth: Int + ) { + // Given + val mockResources = mock() + val fakeConfiguration = Configuration().apply { + smallestScreenWidthDp = fakeWidth + } + + whenever(mockContext.resources) doReturn mockResources + whenever(mockResources.configuration) doReturn fakeConfiguration + whenever(mockTelephonyManager.phoneType) doReturn TelephonyManager.PHONE_TYPE_NONE + + testedProvider = createProvider() + + // When + val type = testedProvider.deviceType + + // Then + assertThat(type).isEqualTo(DeviceType.OTHER) + } + + // endregion + + // region os version + major version + + @Test + fun `M return full version W osVersion`() { + // Given + testedProvider = createProvider() + + // When + val osVersion = testedProvider.osVersion + + // Then + assertThat(osVersion).isEqualTo(fakeOsVersion) + } + + @Test + fun `M return major version W osMajorVersion { major - minor - patch format}`( + @StringForgery(regex = "[1-9]{1,3}") fakeMajor: String, + @StringForgery(regex = "[1-9]{1,3}") fakeMinor: String, + @StringForgery(regex = "[1-9]{1,3}") fakeHotfix: String + ) { + // Given + fakeOsVersion = "$fakeMajor.$fakeMinor.$fakeHotfix" + testedProvider = createProvider() + + // When + val osMajorVersion = testedProvider.osMajorVersion + + // Then + assertThat(osMajorVersion).isEqualTo(fakeMajor) + } + + @Test + fun `M return major version W osMajorVersion { generic format }`( + @StringForgery(type = StringForgeryType.ALPHA_NUMERICAL) fakeVersion: String + ) { + // Given + fakeOsVersion = fakeVersion + testedProvider = createProvider() + + // When + val osMajorVersion = testedProvider.osMajorVersion + + // Then + assertThat(osMajorVersion).isEqualTo(fakeVersion) + } + + // endregion + + // region device name + + @Test + fun `M return device name W deviceName { brand is blank }`() { + // Given + fakeDeviceBrand = "" + testedProvider = createProvider() + + // When + val deviceName = testedProvider.deviceName + + // Then + assertThat(deviceName).isEqualTo(fakeDeviceModel) + } + + @Test + fun `M return device name W deviceName { model contains brand }`( + @StringForgery fakeBrand: String, + @StringForgery modelPrefix: String, + @StringForgery modelSuffix: String + ) { + // Given + val fakeModel = modelPrefix + fakeBrand.capitalize() + modelSuffix + fakeDeviceModel = fakeModel + fakeDeviceBrand = fakeBrand + testedProvider = createProvider() + + // When + val deviceName = testedProvider.deviceName + + // Then + assertThat(deviceName).isEqualTo(fakeModel) + } + + @Test + fun `M return device name W deviceName { model doesn't contain brand }`( + @StringForgery fakeBrand: String, + @StringForgery fakeModel: String + ) { + // Given + assumeFalse(fakeModel.contains(fakeBrand, ignoreCase = true)) + fakeDeviceBrand = fakeBrand + fakeDeviceModel = fakeModel + testedProvider = createProvider() + + // When + val deviceName = testedProvider.deviceName + + // Then + assertThat(deviceName).isEqualTo("${fakeBrand.capitalize()} $fakeModel") + } + + // endregion + + @Test + fun `M return device brand W deviceBrand { model doesn't contain brand }`( + @StringForgery fakeBrand: String + ) { + // Given + fakeDeviceBrand = fakeBrand + testedProvider = createProvider() + + // When + val deviceBrand = testedProvider.deviceBrand + + // Then + assertThat(deviceBrand).isEqualTo(fakeBrand.capitalize()) + } + + // region private + + private fun createProvider(): AndroidInfoProvider = DefaultAndroidInfoProvider( + mockContext, + fakeDeviceBrand, + fakeDeviceModel, + fakeDeviceId, + fakeOsVersion + ) + + private fun String.capitalize() = replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() + } + + companion object { + + @Suppress("unused", "DEPRECATION") + @JvmStatic + fun phoneTypesWithDescription(): List { + return listOf( + PhoneType("gsm", TelephonyManager.PHONE_TYPE_GSM), + PhoneType("cdma", TelephonyManager.PHONE_TYPE_CDMA), + PhoneType("sip", TelephonyManager.PHONE_TYPE_SIP) + ) + } + } + + data class PhoneType(val name: String, val value: Int) + + // endregion + + // region number of displays + + @Test + fun `M return number of displays W numberOfDisplays { 1 }`( + forge: Forge + ) { + // Given + val listOfStates = listOf( + Display.STATE_OFF, + Display.STATE_VR, + Display.STATE_ON, + Display.STATE_ON_SUSPEND, + Display.STATE_DOZE, + Display.STATE_DOZE_SUSPEND, + Display.STATE_UNKNOWN + ) + + val mockDisplays = mutableListOf() + for (i in 0 until forge.anInt(min = 1, max = 20)) { + val mockDisplay = mock() + whenever(mockDisplay.state) doReturn forge.anElementFrom(listOfStates) + whenever(mockDisplay.displayId) doReturn i + mockDisplays.add(mockDisplay) + } + + val mockDisplayManager = mock() + whenever(mockContext.getSystemService(Context.DISPLAY_SERVICE)) + .thenReturn(mockDisplayManager) + whenever(mockDisplayManager.displays) doReturn mockDisplays.toTypedArray() + val expectedDisplays = mockDisplays.filter { + it.state != Display.STATE_OFF && + it.state != Display.STATE_UNKNOWN + } + + testedProvider = createProvider() + + // When + val numberOfDisplays = testedProvider.numberOfDisplays + + // Then + assertThat(numberOfDisplays).isEqualTo( + expectedDisplays.size + ) + } + + @Test + fun `M return null W numberOfDisplays { could not get display manager }`() { + // Given + whenever(mockContext.getSystemService(Context.DISPLAY_SERVICE)) + .thenReturn(null) + + testedProvider = createProvider() + + // When + val numberOfDisplays = testedProvider.numberOfDisplays + + // Then + assertThat(numberOfDisplays).isNull() + } + + // endregion + + // region locale tests + + @TestTargetApi(Build.VERSION_CODES.N) + @Test + fun `M return locales list W locales { API level 24+ }`( + forge: Forge + ) { + // Given + val collectionSize = (1..10).random() + val fakeLocales = mutableListOf() + val mockLocaleList: LocaleList = mock() + + for (i in 0 until collectionSize) { + val fakeLanguage = forge.aStringMatching("[a-z][a-z]{1}") + val fakeRegion = forge.aStringMatching("[a-z][a-z]{1}") + fakeLocales.add( + Locale.Builder().setLanguage(fakeLanguage).setRegion(fakeRegion).build() + ) + } + + val expectedLanguageTags = fakeLocales.map { + it.toLanguageTag() + } + + for (i in fakeLocales.indices) { + whenever(mockLocaleList.get(eq(i))).thenReturn(fakeLocales[i]) + } + + whenever(mockLocaleList.size()) doReturn fakeLocales.size + + whenever(mockConfiguration.locales) doReturn mockLocaleList + + testedProvider = createProvider() + + // When + val result = testedProvider.locales + + // Then + assertThat(result).isEqualTo(expectedLanguageTags) + } + + @TestTargetApi(Build.VERSION_CODES.M) + @Test + fun `M return single locale W locales { API level below 24 }`( + @StringForgery(regex = "[a-z][a-z]{1}") fakeLanguage: String, + @StringForgery(regex = "[a-z][a-z]{1}") fakeRegion: String + ) { + // Given + val fakeLocale = Locale.Builder().setLanguage(fakeLanguage).setRegion(fakeRegion).build() + @Suppress("DEPRECATION") + mockConfiguration.locale = fakeLocale + + testedProvider = createProvider() + + // When + val result = testedProvider.locales + + // Then + assertThat(result).containsExactly(fakeLocale.toLanguageTag()) + } + + @TestTargetApi(Build.VERSION_CODES.N) + @Test + fun `M return current locale W currentLocale { API level 24+ }`( + @StringForgery(regex = "[a-z][a-z]{1}") fakeLanguage: String, + @StringForgery(regex = "[a-z][a-z]{1}") fakeRegion: String + ) { + // Given + val fakeLocale = Locale.Builder().setLanguage(fakeLanguage).setRegion(fakeRegion).build() + val mockLocaleList = mock() + whenever(mockLocaleList.get(0)) doReturn fakeLocale + whenever(mockLocaleList.size()) doReturn 1 + + whenever(mockConfiguration.locales) doReturn mockLocaleList + + testedProvider = createProvider() + + // When + val result = testedProvider.currentLocale + + // Then + assertThat(result).isEqualTo(fakeLocale.toLanguageTag()) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.M) + fun `M return current locale W currentLocale { API level below 24 }`( + @StringForgery(regex = "[a-z][a-z]{1}") fakeLanguage: String, + @StringForgery(regex = "[a-z][a-z]{1}") fakeRegion: String + ) { + // Given + val fakeLocale = Locale.Builder().setLanguage(fakeLanguage).setRegion(fakeRegion).build() + + @Suppress("DEPRECATION") + mockConfiguration.locale = fakeLocale + + testedProvider = createProvider() + + // When + val result = testedProvider.currentLocale + + // Then + assertThat(result).isEqualTo(fakeLocale.toLanguageTag()) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAppVersionProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAppVersionProviderTest.kt new file mode 100644 index 0000000000..09294f1618 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAppVersionProviderTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.system + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DefaultAppVersionProviderTest { + + private lateinit var testedProvider: AppVersionProvider + + @StringForgery + lateinit var fakeVersion: String + + @BeforeEach + fun setUp() { + testedProvider = DefaultAppVersionProvider(fakeVersion) + } + + @Test + fun `M return initial version W get`() { + assertThat(testedProvider.version).isEqualTo(fakeVersion) + } + + @Test + fun `M return a new version W set() + get()`( + @StringForgery fakeNewVersion: String + ) { + // When + testedProvider.version = fakeNewVersion + + // Then + assertThat(testedProvider.version).isEqualTo(fakeNewVersion) + } + + @RepeatedTest(10) + fun `M return a new version W set + get() { multi-threaded }`( + @StringForgery fakeNewVersion: String + ) { + // Given + val lock = CountDownLatch(1) + + // When + Thread { + testedProvider.version = fakeNewVersion + lock.countDown() + }.start() + lock.await() + + // Then + assertThat(testedProvider.version).isEqualTo(fakeNewVersion) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/AbstractExecutorServiceTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/AbstractExecutorServiceTest.kt new file mode 100644 index 0000000000..904015772c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/AbstractExecutorServiceTest.kt @@ -0,0 +1,208 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureMitigation +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.quality.Strictness +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutorService + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal abstract class AbstractExecutorServiceTest { + + lateinit var testedExecutor: T + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockOnThresholdReached: () -> Unit + + @Mock + lateinit var mockOnItemDropped: (Any) -> Unit + + @IntForgery(8, 128) + var fakeBackPressureCapacity: Int = 0 + + @Forgery + lateinit var fakeBackPressureMitigation: BackPressureMitigation + + lateinit var fakeBackpressureStrategy: BackPressureStrategy + + @BeforeEach + fun `set up`(forge: Forge) { + fakeBackpressureStrategy = BackPressureStrategy( + fakeBackPressureCapacity, + mockOnThresholdReached, + mockOnItemDropped, + fakeBackPressureMitigation + ) + testedExecutor = createTestedExecutorService(forge, fakeBackpressureStrategy) + } + + @AfterEach + fun `tear down`() { + testedExecutor.shutdownNow() + } + + abstract fun createTestedExecutorService(forge: Forge, backPressureStrategy: BackPressureStrategy): T + + // region execute + + @Test + fun `M log nothing W execute() { task completes normally }`() { + // When + testedExecutor.execute { + // no-op + } + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log nothing W execute() { worker thread was interrupted }`() { + // When + testedExecutor.execute { + Thread.currentThread().interrupt() + } + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log error + exception W execute() { task throws an exception }`( + forge: Forge + ) { + // Given + val throwable = forge.aThrowable() + + // When + testedExecutor.execute { + throw throwable + } + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + ERROR_UNCAUGHT_EXECUTION_EXCEPTION, + throwable + ) + } + + // endregion + + // region submit + + @Test + fun `M log nothing W submit() { task completes normally }`() { + // When + val futureTask = testedExecutor.submit { + // no-op + } + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isDone).isTrue + + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log nothing W submit() { worker thread was interrupted }`() { + // When + val futureTask = testedExecutor.submit { + Thread.currentThread().interrupt() + } + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isDone).isTrue + + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log error + exception W submit() { task throws an exception }`( + forge: Forge + ) { + // Given + val throwable = forge.aThrowable() + + // When + val futureTask = testedExecutor.submit { + throw throwable + } + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isDone).isTrue + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + ERROR_UNCAUGHT_EXECUTION_EXCEPTION, + throwable + ) + } + + @Test + fun `M log error + exception W submit() { task was cancelled }`() { + // When + val futureTask = testedExecutor.submit { + Thread.sleep(500) + } + futureTask.cancel(true) + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isCancelled).isTrue + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + ERROR_UNCAUGHT_EXECUTION_EXCEPTION, + CancellationException::class.java + ) + } + + // endregion + + companion object { + const val DEFAULT_SLEEP_DURATION_MS = 50L + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/BackPressureExecutorServiceTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/BackPressureExecutorServiceTest.kt new file mode 100644 index 0000000000..93b01d2159 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/BackPressureExecutorServiceTest.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.core.configuration.BackPressureStrategy +import fr.xgouchet.elmyr.Forge +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class BackPressureExecutorServiceTest : + AbstractExecutorServiceTest() { + + override fun createTestedExecutorService( + forge: Forge, + backPressureStrategy: BackPressureStrategy + ): BackPressureExecutorService { + return BackPressureExecutorService( + mockInternalLogger, + forge.anAlphabeticalString(), + backPressureStrategy + ) + } + + @Test + fun `M use DatadogThreadFactory W constructor()`() { + // Then + assertThat(testedExecutor.threadFactory).isInstanceOf(DatadogThreadFactory::class.java) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/DatadogThreadFactoryTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/DatadogThreadFactoryTest.kt new file mode 100644 index 0000000000..6008204db8 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/DatadogThreadFactoryTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions + +@Extensions( + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +internal class DatadogThreadFactoryTest { + + @Test + fun `M create a new thread W newThread()`( + @StringForgery fakeNewThreadContext: String + ) { + // Given + val testedFactory = DatadogThreadFactory(fakeNewThreadContext) + + // When + val thread = testedFactory.newThread {} + + // Then + assertThat(thread.name).isEqualTo("datadog-$fakeNewThreadContext-thread-1") + assertThat(thread.isDaemon).isFalse() + assertThat(thread.priority).isEqualTo(Thread.NORM_PRIORITY) + } + + @Test + fun `M create a new thread with incremented index W newThread()`( + @StringForgery fakeNewThreadContext: String + ) { + // Given + val testedFactory = DatadogThreadFactory(fakeNewThreadContext) + + // When + testedFactory.newThread {} + val thread = testedFactory.newThread {} + + // Then + assertThat(thread.name).isEqualTo("datadog-$fakeNewThreadContext-thread-2") + assertThat(thread.isDaemon).isFalse() + assertThat(thread.priority).isEqualTo(Thread.NORM_PRIORITY) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/DropOldestBackPressuredBlockingQueueTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/DropOldestBackPressuredBlockingQueueTest.kt new file mode 100644 index 0000000000..313662a56f --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/DropOldestBackPressuredBlockingQueueTest.kt @@ -0,0 +1,430 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureMitigation +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.quality.Strictness +import java.lang.Thread.sleep +import java.util.concurrent.BlockingQueue +import java.util.concurrent.TimeUnit +import kotlin.math.min + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class DropOldestBackPressuredBlockingQueueTest { + + lateinit var testedQueue: BlockingQueue + + @Mock + lateinit var mockLogger: InternalLogger + + @Mock + lateinit var mockOnThresholdReached: () -> Unit + + @Mock + lateinit var mockOnItemsDropped: (Any) -> Unit + + @IntForgery(8, 16) + var fakeBackPressureThreshold: Int = 0 + + @BeforeEach + fun `set up`(forge: Forge) { + testedQueue = BackPressuredBlockingQueue( + mockLogger, + forge.anAlphabeticalString(), + BackPressureStrategy( + fakeBackPressureThreshold, + mockOnThresholdReached, + mockOnItemsDropped, + BackPressureMitigation.DROP_OLDEST + ) + ) + } + + // region add(e) + + @Test + fun `M accept item W add() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W add() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W add() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M drop old item and accept W add() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verify(mockOnItemsDropped).invoke(fakeItemList.first()) + } + + // endregion + + // region offer(e) + + @Test + fun `M accept item W offer() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M drop old item and accept W offer() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verify(mockOnItemsDropped).invoke(fakeItemList.first()) + } + + // endregion + + // region offer(e, timeout) + + @Test + fun `M accept item W offer() {empty}`( + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M accept item W offer() { queue already at threshold, waiting for space }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + Thread { + sleep(fakeTimeoutMs - 5) + testedQueue.take() + }.start() + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M drop old item and accept W offer() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verify(mockOnItemsDropped).invoke(fakeItemList.first()) + } + + // endregion + + // region put(e) + + @Test + fun `M accept item W put() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W put() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.put(it) + } + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W put() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.put(fakeItemList[i % fakeItemList.size]) + } + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M wait and accept W put() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.put(fakeItemList[i % fakeItemList.size]) + } + + // When + Thread { + // put() inserts the specified element into this queue, waiting if necessary for space to become available. + // In order to not wait indefinitely, we need to remove an element + sleep(100) + testedQueue.take() + }.start() + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/IgnoreNewestBackPressuredBlockingQueueTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/IgnoreNewestBackPressuredBlockingQueueTest.kt new file mode 100644 index 0000000000..bafa652a60 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/IgnoreNewestBackPressuredBlockingQueueTest.kt @@ -0,0 +1,430 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureMitigation +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.quality.Strictness +import java.lang.Thread.sleep +import java.util.concurrent.BlockingQueue +import java.util.concurrent.TimeUnit +import kotlin.math.min + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class IgnoreNewestBackPressuredBlockingQueueTest { + + lateinit var testedQueue: BlockingQueue + + @Mock + lateinit var mockLogger: InternalLogger + + @Mock + lateinit var mockOnThresholdReached: () -> Unit + + @Mock + lateinit var mockOnItemsDropped: (Any) -> Unit + + @IntForgery(8, 16) + var fakeBackPressureThreshold: Int = 0 + + @BeforeEach + fun `set up`(forge: Forge) { + testedQueue = BackPressuredBlockingQueue( + mockLogger, + forge.anAlphabeticalString(), + BackPressureStrategy( + fakeBackPressureThreshold, + mockOnThresholdReached, + mockOnItemsDropped, + BackPressureMitigation.IGNORE_NEWEST + ) + ) + } + + // region add(e) + + @Test + fun `M accept item W add() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W add() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W add() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M drop old item and accept W add() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).doesNotContain(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verify(mockOnItemsDropped).invoke(fakeNewItem) + } + + // endregion + + // region offer(e) + + @Test + fun `M accept item W offer() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M drop old item and accept W offer() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).doesNotContain(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verify(mockOnItemsDropped).invoke(fakeNewItem) + } + + // endregion + + // region offer(e, timeout) + + @Test + fun `M accept item W offer() {empty}`( + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M accept item W offer() { queue already at threshold, waiting for space }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + Thread { + sleep(fakeTimeoutMs - 5) + testedQueue.take() + }.start() + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M drop old item and accept W offer() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).doesNotContain(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verify(mockOnItemsDropped).invoke(fakeNewItem) + } + + // endregion + + // region put(e) + + @Test + fun `M accept item W put() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W put() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeBackPressureThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.put(it) + } + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W put() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeBackPressureThreshold) { + testedQueue.put(fakeItemList[i % fakeItemList.size]) + } + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M wait and accept W put() { queue already at threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeBackPressureThreshold) { + testedQueue.put(fakeItemList[i % fakeItemList.size]) + } + + // When + Thread { + // put() inserts the specified element into this queue, waiting if necessary for space to become available. + // In order to not wait indefinitely, we need to remove an element + sleep(100) + testedQueue.take() + }.start() + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(fakeBackPressureThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutorTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutorTest.kt new file mode 100644 index 0000000000..003bb357bd --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutorTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.verifyNoInteractions +import java.util.concurrent.CancellationException +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit + +internal class LoggingScheduledThreadPoolExecutorTest : + AbstractExecutorServiceTest() { + + override fun createTestedExecutorService( + forge: Forge, + backPressureStrategy: BackPressureStrategy + ): ScheduledThreadPoolExecutor { + return LoggingScheduledThreadPoolExecutor( + 1, + forge.anAlphabeticalString(), + mockInternalLogger, + backPressureStrategy + ) + } + + @Test + fun `M log nothing W schedule() { task completes normally }`() { + // When + val futureTask = testedExecutor.schedule({ + // no-op + }, 1, TimeUnit.MILLISECONDS) + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isDone).isTrue + + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log nothing W schedule() { worker thread was interrupted }`() { + // When + val futureTask = testedExecutor.submit { + Thread.currentThread().interrupt() + } + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isDone).isTrue + + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log error + exception W schedule() { task throws an exception }`( + forge: Forge + ) { + // Given + val throwable = forge.aThrowable() + + // When + val futureTask = testedExecutor.schedule({ + throw throwable + }, 1, TimeUnit.MILLISECONDS) + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isDone).isTrue + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + ERROR_UNCAUGHT_EXECUTION_EXCEPTION, + throwable + ) + } + + @Test + fun `M log error + exception W schedule() { task is cancelled }`() { + // When + val futureTask = testedExecutor.schedule({ + Thread.sleep(500) + }, 1, TimeUnit.MILLISECONDS) + futureTask.cancel(true) + Thread.sleep(DEFAULT_SLEEP_DURATION_MS) + + // Then + assertThat(futureTask.isCancelled).isTrue + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + ERROR_UNCAUGHT_EXECUTION_EXCEPTION, + CancellationException::class.java + ) + } + + @Test + fun `M use DatadogThreadFactory W constructor()`() { + // Then + assertThat(testedExecutor.threadFactory).isInstanceOf(DatadogThreadFactory::class.java) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/NotifyOnlyUnboundedBackPressuredBlockingQueueTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/NotifyOnlyUnboundedBackPressuredBlockingQueueTest.kt new file mode 100644 index 0000000000..8612705920 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/NotifyOnlyUnboundedBackPressuredBlockingQueueTest.kt @@ -0,0 +1,421 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.quality.Strictness +import java.lang.Thread.sleep +import java.util.concurrent.BlockingQueue +import java.util.concurrent.TimeUnit +import kotlin.math.min + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class NotifyOnlyUnboundedBackPressuredBlockingQueueTest { + + lateinit var testedQueue: BlockingQueue + + @Mock + lateinit var mockLogger: InternalLogger + + @Mock + lateinit var mockOnThresholdReached: () -> Unit + + @Mock + lateinit var mockOnItemsDropped: (Any) -> Unit + + @IntForgery(8, 16) + var fakeNotifyThreshold: Int = 0 + + @BeforeEach + fun `set up`(forge: Forge) { + testedQueue = BackPressuredBlockingQueue( + mockLogger, + executorContext = forge.anAlphabeticalString(), + notifyThreshold = fakeNotifyThreshold, + capacity = Int.MAX_VALUE, + onThresholdReached = mockOnThresholdReached, + onItemDropped = mockOnItemsDropped, + backpressureMitigation = null + ) + } + + // region add(e) + + @Test + fun `M accept item W add() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W add() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeNotifyThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W add() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeNotifyThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeNotifyThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M accept item W add() { queue at notify threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeNotifyThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.add(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeNotifyThreshold + 1) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + // endregion + + // region offer(e) + + @Test + fun `M accept item W offer() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeNotifyThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeNotifyThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeNotifyThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M accept item W offer() { queue at notify threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeNotifyThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeNotifyThreshold + 1) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + // endregion + + // region offer(e, timeout) + + @Test + fun `M accept item W offer() {empty}`( + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + val previousCount = min(fakeNotifyThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.add(it) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W offer() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 1 until fakeNotifyThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeNotifyThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M accept item W offer() { queue already at threshold, waiting for space }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 1 until fakeNotifyThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + Thread { + sleep(fakeTimeoutMs - 5) + testedQueue.take() + }.start() + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeNotifyThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M accept item W offer() { queue at notify threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String, + @LongForgery(10, 100) fakeTimeoutMs: Long + ) { + // Given + for (i in 0 until fakeNotifyThreshold) { + testedQueue.add(fakeItemList[i % fakeItemList.size]) + } + + // When + val result = testedQueue.offer(fakeNewItem, fakeTimeoutMs, TimeUnit.MILLISECONDS) + + // Then + assertThat(result).isTrue() + assertThat(testedQueue).hasSize(fakeNotifyThreshold + 1) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + // endregion + + // region put(e) + + @Test + fun `M accept item W put() {empty}`( + @StringForgery fakeNewItem: String + ) { + // Given + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W put() { not reached threshold yet }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + val previousCount = min(fakeNotifyThreshold / 2, fakeItemList.size) + fakeItemList.take(previousCount).forEach { + testedQueue.put(it) + } + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(previousCount + 1) + assertThat(testedQueue).contains(fakeNewItem) + verifyNoInteractions(mockOnItemsDropped, mockOnThresholdReached, mockLogger) + } + + @Test + fun `M accept item W put() { reaching threshold on last item }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 1 until fakeNotifyThreshold) { + testedQueue.put(fakeItemList[i % fakeItemList.size]) + } + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(fakeNotifyThreshold) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + @Test + fun `M accept item W put() { queue at notify threshold }`( + @StringForgery fakeItemList: List, + @StringForgery fakeNewItem: String + ) { + // Given + for (i in 0 until fakeNotifyThreshold) { + testedQueue.put(fakeItemList[i % fakeItemList.size]) + } + + // When + testedQueue.put(fakeNewItem) + + // Then + assertThat(testedQueue).hasSize(fakeNotifyThreshold + 1) + assertThat(testedQueue).contains(fakeNewItem) + verify(mockOnThresholdReached).invoke() + verifyNoInteractions(mockOnItemsDropped) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ObservableBlockingQueueTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ObservableBlockingQueueTest.kt new file mode 100644 index 0000000000..73942b7de3 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ObservableBlockingQueueTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.internal.thread.NamedRunnable +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mockito.mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class ObservableBlockingQueueTest { + + @Test + fun `M return only once non-null map W try to dump multiple times in dump interval`( + forge: Forge + ) { + // Given + val fakeItemCount = forge.aSmallInt() + val fakeFirstTimestamp = forge.aLong(min = 10000) + val fakeSecondTimestamp = fakeFirstTimestamp + forge.aLong(max = 1000) + val fakeThirdTimestamp = fakeSecondTimestamp + forge.aLong(max = 1000) + val fakeTimestamps = + listOf(fakeFirstTimestamp, fakeSecondTimestamp, fakeThirdTimestamp).iterator() + val fakeTimeProvider: () -> Long = { fakeTimestamps.next() } + val testedObservableLinkedBlockingQueue = + ObservableLinkedBlockingQueue(fakeItemCount + 1, fakeTimeProvider) + repeat(fakeItemCount) { + val mockItem = mock() + testedObservableLinkedBlockingQueue.offer(mockItem) + } + + // When + val firstMap = testedObservableLinkedBlockingQueue.dumpQueue() + val secondMap = testedObservableLinkedBlockingQueue.dumpQueue() + val thirdMap = testedObservableLinkedBlockingQueue.dumpQueue() + + // Then + assertThat(firstMap).isNotNull + assertThat(secondMap).isNullOrEmpty() + assertThat(thirdMap).isNullOrEmpty() + } + + @Test + fun `M return only twice non-null map W try to dump twice times over dump interval`( + forge: Forge + ) { + // Given + val fakeItemCount = forge.aSmallInt() + val fakeFirstTimestamp = forge.aLong(min = 10000) + val fakeSecondTimestamp = fakeFirstTimestamp + forge.aLong(min = 5000) + val fakeTimestamps = listOf(fakeFirstTimestamp, fakeSecondTimestamp).iterator() + val fakeTimeProvider: () -> Long = { fakeTimestamps.next() } + val testedObservableLinkedBlockingQueue = + ObservableLinkedBlockingQueue(fakeItemCount + 1, fakeTimeProvider) + repeat(fakeItemCount) { + val mockItem = mock() + testedObservableLinkedBlockingQueue.offer(mockItem) + } + + // When + val firstMap = testedObservableLinkedBlockingQueue.dumpQueue() + val secondMap = testedObservableLinkedBlockingQueue.dumpQueue() + + // Then + assertThat(firstMap).isNotNull + assertThat(secondMap).isNotNull + } + + @Test + fun `M build correct map W try to dump named runnable`( + forge: Forge + ) { + // Given + val expectedMap = mutableMapOf() + val fakeTimeProvider: () -> Long = { forge.aLong(min = 10000L) } + val fakeRunnableCount = forge.anInt(min = 5, max = 100) + val testedObservableLinkedBlockingQueue = + ObservableLinkedBlockingQueue(fakeRunnableCount + 1, fakeTimeProvider) + val fakeRunnableTypeCount = forge.anInt(min = 1, max = fakeRunnableCount) + val fakeRunnableTypes = mutableListOf() + repeat(fakeRunnableTypeCount) { + fakeRunnableTypes.add(forge.anAlphabeticalString()) + } + repeat(fakeRunnableCount) { + val fakeName = forge.anElementFrom(fakeRunnableTypes) + val fakeNamedRunnable = NamedRunnable(fakeName, mock()) + testedObservableLinkedBlockingQueue.offer(fakeNamedRunnable) + expectedMap[fakeName] = (expectedMap[fakeName] ?: 0) + 1 + } + + // When + val map = testedObservableLinkedBlockingQueue.dumpQueue() + + // Then + assertThat(map).isEqualTo(expectedMap) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadExtTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadExtTest.kt new file mode 100644 index 0000000000..8a58746d70 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadExtTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.tools.unit.forge.BaseConfigurator +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import kotlin.system.measureTimeMillis + +@Extensions( + ExtendWith( + MockitoExtension::class, + ForgeExtension::class + ) +) +@ForgeConfiguration(value = BaseConfigurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class ThreadExtTest { + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Test + fun `M sleep for given duration W sleepSafe()`( + @LongForgery(min = 100L, max = 1000L) duration: Long + ) { + // When + val wasInterrupted: Boolean + val actualDuration = measureTimeMillis { + wasInterrupted = sleepSafe(duration, mockInternalLogger) + } + + // Then + assertThat(wasInterrupted).isFalse() + assertThat(actualDuration).isCloseTo(duration, Offset.offset(10L)) + } + + @Test + fun `M swallow error W sleepSafe() {negative duration}`( + @LongForgery(max = -1L) duration: Long + ) { + // When + val wasInterrupted = sleepSafe(duration, mockInternalLogger) + + // Then + assertThat(wasInterrupted).isFalse() + } + + @Test + fun `M swallow interruption W sleepSafe()`() { + // Given + val currentThread = Thread.currentThread() + val otherThread = Thread { + Thread.sleep(50L) + currentThread.interrupt() + } + + // When + otherThread.start() + val wasInterrupted = sleepSafe(250, mockInternalLogger) + + // Then + assertThat(wasInterrupted).isTrue() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExtTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExtTest.kt new file mode 100644 index 0000000000..f25b6dfe1b --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExtTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.thread + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.ThreadPoolExecutor +import kotlin.system.measureTimeMillis + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ThreadPoolExecutorExtTest { + + @Mock + lateinit var testedMockExecutor: ThreadPoolExecutor + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Test + fun `M return false W waitToIdle { timeout reached }`( + @LongForgery(min = 0, max = 500) fakeTimeout: Long, + forge: Forge + ) { + // GIVEN + val fakeTaskCount = forge.aLong(min = 2, max = 10) + val fakeCompletedCount = forge.aLong(min = 0, max = fakeTaskCount - 1) + whenever(testedMockExecutor.taskCount).thenReturn(fakeTaskCount) + whenever(testedMockExecutor.completedTaskCount).thenReturn(fakeCompletedCount) + + // WHEN + val isIdled = testedMockExecutor.waitToIdle(fakeTimeout, mockInternalLogger) + testedMockExecutor.isIdle() + + // THEN + assertThat(isIdled).isFalse() + } + + @Test + fun `M wait max timeout milliseconds W waitToIdle { executor not idled }`( + @LongForgery(min = 500, max = 1000) fakeTimeout: Long, + forge: Forge + ) { + // GIVEN + val fakeTaskCount = forge.aLong(min = 2, max = 10) + val fakeCompletedCount = forge.aLong(min = 0, max = fakeTaskCount - 1) + whenever(testedMockExecutor.taskCount).thenReturn(fakeTaskCount) + whenever(testedMockExecutor.completedTaskCount).thenReturn(fakeCompletedCount) + + // WHEN + val duration = measureTimeMillis { + testedMockExecutor.waitToIdle(fakeTimeout, mockInternalLogger) + } + + // THEN + assertThat(duration).isCloseTo(fakeTimeout, Offset.offset(130)) + } + + @Test + fun `M return true W waitToIdle { executor idled }`( + @LongForgery(min = 0, max = 500) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long + + ) { + // GIVEN + whenever(testedMockExecutor.taskCount).thenReturn(fakeTaskCount) + whenever(testedMockExecutor.completedTaskCount) + .thenReturn(fakeTaskCount) + + // WHEN + val isIdled = testedMockExecutor.waitToIdle(fakeTimeout, mockInternalLogger) + + // THEN + assertThat(isIdled).isTrue() + } + + @Test + fun `M return true W waitToIdle { executor idled after multiple iterations }`( + @LongForgery( + min = MAX_SLEEP_DURATION_IN_MS * 3, + max = MAX_SLEEP_DURATION_IN_MS * 4 + ) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long + + ) { + // GIVEN + whenever(testedMockExecutor.taskCount).thenReturn(fakeTaskCount) + whenever(testedMockExecutor.completedTaskCount) + .thenReturn(fakeTaskCount / 2).thenReturn(fakeTaskCount) + + // WHEN + val isIdled = testedMockExecutor.waitToIdle(fakeTimeout, internalLogger = mock()) + + // THEN + assertThat(isIdled).isTrue() + } + + @Test + fun `M return false W waitToIdle { timeout is negative, executor not idled }`( + @LongForgery(min = Long.MIN_VALUE, max = 0) fakeTimeout: Long, + forge: Forge + ) { + // WHEN + val fakeTaskCount = forge.aLong(min = 2, max = 10) + val fakeCompletedCount = forge.aLong(min = 0, max = fakeTaskCount - 1) + whenever(testedMockExecutor.taskCount).thenReturn(fakeTaskCount) + whenever(testedMockExecutor.completedTaskCount).thenReturn(fakeCompletedCount) + val isIdled = testedMockExecutor.waitToIdle(fakeTimeout, mockInternalLogger) + + // THEN + assertThat(isIdled).isFalse() + } + + @Test + fun `M return true W waitToIdle { timeout is negative, executor idled }`( + @LongForgery(min = Long.MIN_VALUE, max = 0) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long + ) { + // GIVEN + whenever(testedMockExecutor.taskCount).thenReturn(fakeTaskCount) + whenever(testedMockExecutor.completedTaskCount) + .thenReturn(fakeTaskCount) + + // WHEN + val isIdled = testedMockExecutor.waitToIdle(fakeTimeout, mockInternalLogger) + + // THEN + assertThat(isIdled).isTrue() + } + + @Test + fun `M return true W waitToIdle { more tasks where added between sleep intervals }`( + @LongForgery( + min = MAX_SLEEP_DURATION_IN_MS * 3, + max = MAX_SLEEP_DURATION_IN_MS * 4 + ) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long + ) { + // GIVEN + whenever(testedMockExecutor.taskCount) + .thenReturn(fakeTaskCount) + .thenReturn(fakeTaskCount + 2) + whenever(testedMockExecutor.completedTaskCount) + .thenReturn(fakeTaskCount / 2) + .thenReturn(fakeTaskCount + 2) + + // WHEN + val isIdled = testedMockExecutor.waitToIdle(fakeTimeout, mockInternalLogger) + + // THEN + assertThat(isIdled).isTrue() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProviderTest.kt new file mode 100644 index 0000000000..bb01164101 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProviderTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.time + +import android.os.Build +import android.os.Process +import android.os.SystemClock +import com.datadog.android.core.internal.system.BuildSdkVersionProvider +import com.datadog.android.rum.DdRumContentProvider +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(ForgeExtension::class) +) +class DefaultAppStartTimeProviderTest { + @Test + fun `M return process start time W appStartTime { N+ }`( + @IntForgery(min = Build.VERSION_CODES.N) apiVersion: Int + ) { + // GIVEN + val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() + whenever(mockBuildSdkVersionProvider.version) doReturn apiVersion + val diffMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime() + val startTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(diffMs) + + // WHEN + val timeProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) + val providedStartTime = timeProvider.appStartTimeNs + + // THEN + assertThat(providedStartTime) + .isCloseTo(startTimeNs, Offset.offset(TimeUnit.MILLISECONDS.toNanos(100))) + } + + @Test + fun `M return content provider load time W appStartTime { Legacy }`( + @IntForgery(min = Build.VERSION_CODES.M, max = Build.VERSION_CODES.N) apiVersion: Int + ) { + // GIVEN + val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() + whenever(mockBuildSdkVersionProvider.version) doReturn apiVersion + val startTimeNs = DdRumContentProvider.createTimeNs + + // WHEN + val timeProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) + val providedStartTime = timeProvider.appStartTimeNs + + // THEN + assertThat(providedStartTime).isEqualTo(startTimeNs) + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/time/KronosTimeProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/KronosTimeProviderTest.kt similarity index 76% rename from dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/time/KronosTimeProviderTest.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/KronosTimeProviderTest.kt index 62aa806661..09be35bcfb 100644 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/time/KronosTimeProviderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/KronosTimeProviderTest.kt @@ -8,13 +8,9 @@ package com.datadog.android.core.internal.time import com.datadog.android.utils.forge.Configurator import com.lyft.kronos.Clock -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.whenever import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Date -import java.util.concurrent.TimeUnit import org.assertj.core.api.Assertions.assertThat import org.assertj.core.data.Offset import org.junit.jupiter.api.BeforeEach @@ -24,7 +20,11 @@ import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.Date +import java.util.concurrent.TimeUnit @Extensions( ExtendWith(MockitoExtension::class), @@ -56,14 +56,26 @@ internal class KronosTimeProviderTest { } @Test - fun `returns server time offset`() { + fun `returns server time offset in nanoseconds`() { val now = System.currentTimeMillis() val result = testedTimeProvider.getServerOffsetNanos() val expectedOffset = TimeUnit.MILLISECONDS.toNanos(fakeDate.time - now) assertThat(result).isCloseTo( expectedOffset, - Offset.offset(TimeUnit.MILLISECONDS.toNanos(1)) + Offset.offset(TimeUnit.MILLISECONDS.toNanos(TEST_OFFSET)) + ) + } + + @Test + fun `returns server time offset in milliseconds`() { + val now = System.currentTimeMillis() + val result = testedTimeProvider.getServerOffsetMillis() + + val expectedOffset = fakeDate.time - now + assertThat(result).isCloseTo( + expectedOffset, + Offset.offset(TEST_OFFSET) ) } @@ -72,6 +84,10 @@ internal class KronosTimeProviderTest { val now = System.currentTimeMillis() val result = testedTimeProvider.getDeviceTimestamp() - assertThat(result).isCloseTo(now, Offset.offset(50L)) + assertThat(result).isCloseTo(now, Offset.offset(TEST_OFFSET)) + } + + companion object { + const val TEST_OFFSET = 10L } } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/LoggingSyncListenerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/LoggingSyncListenerTest.kt new file mode 100644 index 0000000000..4796049939 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/LoggingSyncListenerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.time + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.kotlin.mock + +@Extensions( + ExtendWith(ForgeExtension::class) +) +internal class LoggingSyncListenerTest { + + @Test + fun `M log error W onError()`( + @StringForgery(regex = "https://[a-z]+\\.com") fakeHost: String, + forge: Forge + ) { + // Given + val mockInternalLogger = mock() + val testableListener = LoggingSyncListener(internalLogger = mockInternalLogger) + val throwable = forge.aThrowable() + + // When + testableListener.onError(fakeHost, throwable) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + "Kronos onError @host:$fakeHost", + throwable + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/user/DatadogUserInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/user/DatadogUserInfoProviderTest.kt new file mode 100644 index 0000000000..39365c696e --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/user/DatadogUserInfoProviderTest.kt @@ -0,0 +1,351 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.user + +import com.datadog.android.api.context.UserInfo +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.MapForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@ExtendWith(ForgeExtension::class) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class DatadogUserInfoProviderTest { + + private lateinit var testedProvider: DatadogUserInfoProvider + + @BeforeEach + fun `set up`() { + testedProvider = DatadogUserInfoProvider() + } + + @Test + fun `M return default userInfo W getUserInfo()`() { + // When + val result = testedProvider.getUserInfo() + + // Then + assertThat(result).isEqualTo(UserInfo()) + } + + @Test + fun `M return saved userInfo W setUserInfo() and getUserInfo()`( + @Forgery userInfo: UserInfo, + @StringForgery userId: String + ) { + // Given + val nonNullUserId = userInfo.id ?: userId + val validUserInfo = userInfo.copy(id = nonNullUserId) + + // When + testedProvider.setUserInfo(nonNullUserId, userInfo.name, userInfo.email, userInfo.additionalProperties) + val result = testedProvider.getUserInfo() + + // Then + assertThat(result).isEqualTo(validUserInfo) + } + + @Test + fun `M keep existing properties W addUserProperties() is called`( + @Forgery userInfo: UserInfo, + @StringForgery userId: String, + forge: Forge + ) { + // Given + val customProperties = forge.exhaustiveAttributes() + val nonNullUserId = userInfo.id ?: userId + testedProvider.setUserInfo(nonNullUserId, userInfo.name, userInfo.email, customProperties) + + // When + testedProvider.addUserProperties(customProperties) + + // Then + assertThat( + testedProvider.getUserInfo().additionalProperties + ).isEqualTo(customProperties) + } + + @Test + fun `M use immutable properties W addUserProperties() is called { changing properties values }`( + forge: Forge + ) { + // Given + val fakeProperties = forge.exhaustiveAttributes() + val fakeExpectedProperties = fakeProperties.toMap() + val fakeMutableProperties = fakeProperties.toMutableMap() + testedProvider.addUserProperties(fakeMutableProperties) + + // When + fakeMutableProperties.keys.forEach { + fakeMutableProperties[it] = forge.anAlphabeticalString() + } + + // Then + assertThat( + testedProvider.getUserInfo().additionalProperties + ).isEqualTo(fakeExpectedProperties) + } + + @Test + fun `M use immutable properties W addUserProperties() is called { adding properties }`( + forge: Forge + ) { + // Given + val fakeProperties = forge.exhaustiveAttributes() + val fakeExpectedProperties = fakeProperties.toMap() + val fakeMutableProperties = fakeProperties.toMutableMap() + testedProvider.addUserProperties(fakeMutableProperties) + + // When + repeat(forge.anInt(1, 10)) { + fakeMutableProperties[forge.anAlphabeticalString()] = forge.anAlphabeticalString() + } + + // Then + assertThat( + testedProvider.getUserInfo().additionalProperties + ).isEqualTo(fakeExpectedProperties) + } + + @Test + fun `M use immutable properties W addUserProperties() is called { removing properties }`( + forge: Forge + ) { + // Given + val fakeProperties = forge.exhaustiveAttributes() + val fakeExpectedProperties = fakeProperties.toMap() + val fakeMutableProperties = fakeProperties.toMutableMap() + testedProvider.addUserProperties(fakeMutableProperties) + + // When + repeat(forge.anInt(1, fakeMutableProperties.size + 1)) { + fakeMutableProperties.remove(fakeMutableProperties.keys.random()) + } + + // Then + assertThat( + testedProvider.getUserInfo().additionalProperties + ).isEqualTo(fakeExpectedProperties) + } + + @Test + fun `M keep new property key W addUserProperties() is called and the key already exists`( + @Forgery userInfo: UserInfo, + @StringForgery userId: String, + @StringForgery key: String, + @StringForgery value1: String, + @StringForgery value2: String + ) { + // Given + val nonNullUserId = userInfo.id ?: userId + testedProvider.setUserInfo(nonNullUserId, userInfo.name, userInfo.email, mutableMapOf(key to value1)) + + // When + testedProvider.addUserProperties(mapOf(key to value2)) + + // Then + assertThat( + testedProvider.getUserInfo().additionalProperties + ).isEqualTo( + mapOf(key to value2) + ) + } + + @Test + fun `M use immutable values W setUserInfo { changing properties values }()`( + forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + val fakeMutableUserProperties = fakeUserProperties.toMutableMap() + val fakeExpectedUserProperties = fakeUserProperties.toMap() + testedProvider.setUserInfo(id, name, email, fakeMutableUserProperties) + + // When + fakeMutableUserProperties.keys.forEach { key -> + fakeMutableUserProperties[key] = forge.anAlphaNumericalString() + } + + // Then + assertThat(testedProvider.getUserInfo().additionalProperties).isEqualTo(fakeExpectedUserProperties) + } + + @Test + fun `M use immutable values W setUserInfo { adding properties }()`( + forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + val fakeMutableUserProperties = fakeUserProperties.toMutableMap() + val fakeExpectedUserProperties = fakeUserProperties.toMap() + testedProvider.setUserInfo(id, name, email, fakeMutableUserProperties) + + // When + repeat(forge.anInt(1, 10)) { + fakeMutableUserProperties[forge.anAlphabeticalString()] = forge.anAlphabeticalString() + } + + // Then + assertThat(testedProvider.getUserInfo().additionalProperties).isEqualTo(fakeExpectedUserProperties) + } + + @Test + fun `M enriches empty user info with anonymousId W setAnonymousId`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String + ) { + // When + testedProvider.setAnonymousId(anonymousId) + + // Then + testedProvider.getUserInfo().let { + assertThat(it.anonymousId).isEqualTo(anonymousId) + assertThat(it.id).isNull() + assertThat(it.name).isNull() + assertThat(it.email).isNull() + assertThat(it.additionalProperties).isEmpty() + } + } + + @Test + fun `M enriches existing user info with anonymousId W setAnonymousId`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + testedProvider.setUserInfo(id, name, email, fakeUserProperties) + + // When + testedProvider.setAnonymousId(anonymousId) + + // Then + testedProvider.getUserInfo().let { + assertThat(it.anonymousId).isEqualTo(anonymousId) + assertThat(it.id).isEqualTo(id) + assertThat(it.name).isEqualTo(name) + assertThat(it.email).isEqualTo(email) + assertThat(it.additionalProperties).isEqualTo(fakeUserProperties) + } + } + + @Test + fun `M clears the anonymousId W setAnonymousId { null }`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String + ) { + // Given + testedProvider.setAnonymousId(anonymousId) + + // When + testedProvider.setAnonymousId(null) + + // Then + assertThat(testedProvider.getUserInfo().anonymousId).isNull() + } + + @Test + fun `M clears the anonymousId and keeps existing user info W setAnonymousId { null }`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + testedProvider.setUserInfo(id, name, email, fakeUserProperties) + testedProvider.setAnonymousId(anonymousId) + + // When + testedProvider.setAnonymousId(null) + + // Then + testedProvider.getUserInfo().let { + assertThat(it.anonymousId).isNull() + assertThat(it.id).isEqualTo(id) + assertThat(it.name).isEqualTo(name) + assertThat(it.email).isEqualTo(email) + assertThat(it.additionalProperties).isEqualTo(fakeUserProperties) + } + } + + @Test + fun `M use immutable values W setUserInfo { removing properties }()`( + forge: Forge, + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + val fakeMutableUserProperties = fakeUserProperties.toMutableMap() + val fakeExpectedUserProperties = fakeUserProperties.toMap() + testedProvider.setUserInfo(id, name, email, fakeMutableUserProperties) + + // When + repeat(forge.anInt(1, fakeMutableUserProperties.size + 1)) { + fakeMutableUserProperties.remove(fakeMutableUserProperties.keys.random()) + } + + // Then + assertThat(testedProvider.getUserInfo().additionalProperties).isEqualTo(fakeExpectedUserProperties) + } + + @Test + fun `M return default userInfo W clearUserInfo()`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, + @StringForgery name: String, + @StringForgery(regex = "\\w+@\\w+") email: String, + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]) + ) fakeUserProperties: Map + ) { + // Given + testedProvider.setUserInfo(id, name, email, fakeUserProperties) + + // When + testedProvider.clearUserInfo() + + // Then + assertThat(testedProvider.getUserInfo()).isEqualTo(UserInfo()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ByteArrayExtTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ByteArrayExtTest.kt new file mode 100644 index 0000000000..15b5aec3a6 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ByteArrayExtTest.kt @@ -0,0 +1,623 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.datadog.android.api.InternalLogger +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.verifyNoInteractions +import kotlin.math.max + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +internal class ByteArrayExtTest { + + @Mock + lateinit var mockInternalLogger: InternalLogger + + // region split() + + @Test + fun `M splits a byteArray W split() {0 separator}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) separator: String, + @StringForgery(StringForgeryType.NUMERICAL) rawString: String + ) { + // Given + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val subs = byteArray.split(separator.first().code.toByte(), mockInternalLogger) + + // Then + assertThat(subs).hasSize(1) + assertThat(subs[0]).isEqualTo(byteArray) + } + + @Test + fun `M splits a byteArray W split() {1 separator}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) separator: String, + @StringForgery(StringForgeryType.NUMERICAL) part0: String, + @StringForgery(StringForgeryType.NUMERICAL) part1: String + ) { + // Given + val rawString = part0 + separator + part1 + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val subs = byteArray.split(separator.first().code.toByte(), mockInternalLogger) + + // Then + assertThat(subs).hasSize(2) + assertThat(String(subs[0])).isEqualTo(part0) + assertThat(String(subs[1])).isEqualTo(part1) + } + + @Test + fun `M splits a byteArray W split() {trailing separator}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) separator: String, + @StringForgery(StringForgeryType.NUMERICAL) part0: String + ) { + // Given + val rawString = part0 + separator + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val subs = byteArray.split(separator.first().code.toByte(), mockInternalLogger) + + // Then + assertThat(subs).hasSize(1) + assertThat(String(subs[0])).isEqualTo(part0) + } + + @Test + fun `M splits a byteArray W split() {leading separator}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) separator: String, + @StringForgery(StringForgeryType.NUMERICAL) part0: String + ) { + // Given + val rawString = separator + part0 + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val subs = byteArray.split(separator.first().code.toByte(), mockInternalLogger) + + // Then + assertThat(subs).hasSize(1) + assertThat(String(subs[0])).isEqualTo(part0) + } + + @Test + fun `M splits a byteArray W split() {consecutive separators}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) separator: String, + @StringForgery(StringForgeryType.NUMERICAL) part0: String, + @StringForgery(StringForgeryType.NUMERICAL) part1: String + ) { + // Given + val rawString = part0 + separator + separator + part1 + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val subs = byteArray.split(separator.first().code.toByte(), mockInternalLogger) + + // Then + assertThat(subs).hasSize(2) + assertThat(String(subs[0])).isEqualTo(part0) + assertThat(String(subs[1])).isEqualTo(part1) + } + + // endregion + + // region indexOf() + + @Test + fun `M returns -1 W indexOf() {invalid start}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) searchedChar: String, + @StringForgery(StringForgeryType.ALPHABETICAL) rawString: String + ) { + // Given + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val index = byteArray.indexOf(searchedChar.first().code.toByte(), -1) + + // Then + assertThat(index).isEqualTo(-1) + } + + @Test + fun `M returns -1 W indexOf() {byte not found}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) searchedChar: String, + @StringForgery(StringForgeryType.NUMERICAL) rawString: String + ) { + // Given + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val index = byteArray.indexOf(searchedChar.first().code.toByte(), 0) + + // Then + assertThat(index).isEqualTo(-1) + } + + @Test + fun `M return index W indexOf() {byte found}`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) searchedChar: String, + @StringForgery(StringForgeryType.NUMERICAL) part0: String, + @StringForgery(StringForgeryType.NUMERICAL) part1: String + ) { + // Given + val rawString = part0 + searchedChar + part1 + val byteArray = rawString.toByteArray(Charsets.UTF_8) + val expectedIndex = part0.toByteArray(Charsets.UTF_8).size + + // When + val index = byteArray.indexOf(searchedChar.first().code.toByte(), 0) + + // Then + assertThat(index).isEqualTo(expectedIndex) + } + + @Test + fun `M find all indexes W indexOf()`( + @StringForgery(StringForgeryType.ALPHABETICAL, size = 1) searchedChar: String, + @StringForgery(StringForgeryType.NUMERICAL) parts: List + ) { + // Given + val rawString = parts.joinToString(searchedChar) + val expectedIndexes = mutableListOf() + var prevExpectedIndex = 0 + parts.forEachIndexed { index, s -> + if (index > 0) { + expectedIndexes.add(prevExpectedIndex) + prevExpectedIndex++ + } + prevExpectedIndex += s.toByteArray().size + } + val byteArray = rawString.toByteArray(Charsets.UTF_8) + + // When + val foundIndexes = mutableListOf() + var prevFoundIndex = 0 + do { + val index = byteArray.indexOf(searchedChar.first().code.toByte(), prevFoundIndex) + if (index >= 0) { + foundIndexes.add(index) + prevFoundIndex = index + 1 + } else { + prevFoundIndex = -1 + } + } while (prevFoundIndex >= 0) + + // Then + assertThat(foundIndexes).containsAll(expectedIndexes) + } + + // endregion + + // region join() + + @Test + fun `M join items W join() { no prefix }`( + @StringForgery separator: String, + @StringForgery suffix: String, + forge: Forge + ) { + // Given + val dataBytes = forge.aList { + forge.aString().toByteArray() + } + + val separatorBytes = separator.toByteArray() + val suffixBytes = suffix.toByteArray() + + val expected = dataBytes.reduce { acc, item -> + acc + separatorBytes + item + } + suffixBytes + + // When + val joined = dataBytes.join( + separatorBytes, + suffix = suffixBytes, + internalLogger = mockInternalLogger + ) + + // Then + assertThat(joined).isEqualTo(expected) + } + + @Test + fun `M join items W join() { no suffix }`( + @StringForgery separator: String, + @StringForgery prefix: String, + forge: Forge + ) { + // Given + val dataBytes = forge.aList { + forge.aString().toByteArray() + } + + val separatorBytes = separator.toByteArray() + val prefixBytes = prefix.toByteArray() + + val expected = prefixBytes + dataBytes.reduce { acc, item -> + acc + separatorBytes + item + } + + // When + val joined = dataBytes.join( + separatorBytes, + prefix = prefixBytes, + internalLogger = mockInternalLogger + ) + + // Then + assertThat(joined).isEqualTo(expected) + } + + @Test + fun `M join items W join() { no suffix and prefix }`( + @StringForgery separator: String, + forge: Forge + ) { + // Given + val dataBytes = forge.aList { + forge.aString().toByteArray() + } + + val separatorBytes = separator.toByteArray() + + val expected = dataBytes.reduce { acc, item -> + acc + separatorBytes + item + } + + // When + val joined = dataBytes.join(separatorBytes, internalLogger = mockInternalLogger) + + // Then + assertThat(joined).isEqualTo(expected) + } + + @Test + fun `M join items W join() { empty separator }`( + @StringForgery prefix: String, + @StringForgery suffix: String, + @StringForgery data: List + ) { + // Given + val dataBytes = data.map { it.toByteArray() } + val prefixBytes = prefix.toByteArray() + val suffixBytes = suffix.toByteArray() + + val expected = prefixBytes + dataBytes.reduce { acc, bytes -> + acc + bytes + } + suffixBytes + + // When + val joined = dataBytes.join( + separator = ByteArray(0), + prefix = prefixBytes, + suffix = suffixBytes, + internalLogger = mockInternalLogger + ) + + // Then + assertThat(joined).isEqualTo(expected) + } + + @Test + fun `M join items W join()`( + @StringForgery separator: String, + @StringForgery prefix: String, + @StringForgery suffix: String, + @StringForgery data: List + ) { + // Given + val dataBytes = data.map { it.toByteArray() } + val separatorBytes = separator.toByteArray() + val prefixBytes = prefix.toByteArray() + val suffixBytes = suffix.toByteArray() + + val expected = prefixBytes + dataBytes.reduce { acc, item -> + acc + separatorBytes + item + } + suffixBytes + + // When + val joined = + dataBytes.join( + separator = separatorBytes, + prefix = prefixBytes, + suffix = suffixBytes, + internalLogger = mockInternalLogger + ) + + // Then + assertThat(joined).isEqualTo(expected) + } + + @Test + fun `M join items W join() { no items }`( + @StringForgery separator: String, + @StringForgery prefix: String, + @StringForgery suffix: String + ) { + // Given + val dataBytes = emptyList() + + val separatorBytes = separator.toByteArray() + val prefixBytes = prefix.toByteArray() + val suffixBytes = suffix.toByteArray() + + val expected = prefixBytes + suffixBytes + + // When + val joined = + dataBytes.join( + separator = separatorBytes, + prefix = prefixBytes, + suffix = suffixBytes, + internalLogger = mockInternalLogger + ) + + // Then + assertThat(joined).isEqualTo(expected) + } + + @Test + fun `M join items W join() { single item }`( + @StringForgery separator: String, + @StringForgery prefix: String, + @StringForgery suffix: String, + @StringForgery data: String + ) { + // Given + val dataBytes = listOf(data.toByteArray()) + val separatorBytes = separator.toByteArray() + val prefixBytes = prefix.toByteArray() + val suffixBytes = suffix.toByteArray() + val expected = prefixBytes + dataBytes[0] + suffixBytes + + // When + val joined = + dataBytes.join( + separator = separatorBytes, + prefix = prefixBytes, + suffix = suffixBytes, + internalLogger = mockInternalLogger + ) + + // Then + assertThat(joined).isEqualTo(expected) + } + + @Test + fun `M join items W join() { no prefix, no suffix, no data }`( + @StringForgery separator: String + ) { + // Given + val dataBytes = emptyList() + + // When + val joined = + dataBytes.join(separator = separator.toByteArray(), internalLogger = mockInternalLogger) + + // Then + assertThat(joined).isEmpty() + } + + // endregion + + // region copyTo() + + @Test + fun `M copy data W copyTo() {copy entire content}`( + @StringForgery(StringForgeryType.NUMERICAL) rawString: String + ) { + // Given + val byteArray = rawString.toByteArray(Charsets.UTF_8) + val destination = ByteArray(byteArray.size) + + // When + val result = byteArray.copyTo(0, destination, 0, byteArray.size, mockInternalLogger) + + // Then + assertThat(result).isTrue() + assertThat(byteArray).isEqualTo(destination) + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M copy data W copyTo() {copy partial content}`( + @StringForgery(StringForgeryType.NUMERICAL, size = 64) rawString: String, + @IntForgery(min = 0, max = 32) startIndex: Int, + @IntForgery(min = 33, max = 64) endIndex: Int + ) { + // Given + val byteArray = rawString.toByteArray(Charsets.UTF_8) + val copySize = endIndex - startIndex + val destination = ByteArray(copySize) + + // When + val result = byteArray.copyTo(startIndex, destination, 0, copySize, mockInternalLogger) + + // Then + assertThat(result).isTrue() + for (i in 0 until copySize) { + assertThat(byteArray[startIndex + i]).isEqualTo(destination[i]) + } + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M return false W copyTo() {invalid source size}`( + @StringForgery(StringForgeryType.NUMERICAL) rawString: String, + @IntForgery(min = 1, max = 128) overflow: Int + ) { + // Given + val byteArray = rawString.toByteArray(Charsets.UTF_8) + val destination = ByteArray(byteArray.size + overflow + 1) + + // When + val result = byteArray.copyTo(0, destination, 0, byteArray.size + overflow, mockInternalLogger) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return false W copyTo() {invalid destination size}`( + @StringForgery(StringForgeryType.NUMERICAL) rawString: String, + @IntForgery(min = 1, max = 128) underflow: Int + ) { + // Given + val byteArray = rawString.toByteArray(Charsets.UTF_8) + val destination = ByteArray(max(byteArray.size - underflow, 0)) + + // When + val result = byteArray.copyTo(0, destination, 0, byteArray.size, mockInternalLogger) + + // Then + assertThat(result).isFalse() + } + + // endregion + + // region data I/O + + @Test + fun `M retrieve a short stored in a byte array W toByteArray() + toShort()`( + @IntForgery(min = 0, max = Short.MAX_VALUE.toInt()) i: Int + ) { + // Given + val s = i.toShort() + val byteArray = s.toByteArray() + + // When + val result = byteArray.toShort() + + // Then + assertThat(result).isEqualTo(s) + } + + @Test + fun `M retrieve an int stored in a byte array W toByteArray() + toInt()`( + @IntForgery i: Int + ) { + // Given + val byteArray = i.toByteArray() + + // When + val result = byteArray.toInt() + + // Then + assertThat(result).isEqualTo(i) + } + + @Test + fun `M retrieve a long stored in a byte array W toByteArray() + toLong()`( + @LongForgery l: Long + ) { + // Given + val byteArray = l.toByteArray() + + // When + val result = byteArray.toLong() + + // Then + assertThat(result).isEqualTo(l) + } + + @Test + fun `M return whole byte array W copyOfRangeSafe()`( + @StringForgery(size = 32) data: String + ) { + // Given + val byteArray = data.toByteArray() + + // When + val result = byteArray.copyOfRangeSafe(0, byteArray.size) + + // Then + assertThat(result).isEqualTo(byteArray) + } + + @Test + fun `M return subset byte array W copyOfRangeSafe()`( + @StringForgery(size = 32) prefix: String, + @StringForgery(size = 32) data: String, + @StringForgery(size = 32) postfix: String + ) { + // Given + val prefixByteArray = prefix.toByteArray() + val dataByteArray = data.toByteArray() + val postfixByteArray = postfix.toByteArray() + val byteArray = prefixByteArray + dataByteArray + postfixByteArray + + // When + val result = byteArray.copyOfRangeSafe(prefixByteArray.size, prefixByteArray.size + dataByteArray.size) + + // Then + assertThat(result).isEqualTo(dataByteArray) + } + + @Test + fun `M return an empty byte array W copyOfRangeSafe() { negative index }`( + @StringForgery(size = 32) data: String, + @IntForgery(min = -512, max = 0) negativeIndex: Int + ) { + // Given + val byteArray = data.toByteArray() + + // When + val result = byteArray.copyOfRangeSafe(negativeIndex, 1) + + // Then + assertThat(result).isEmpty() + } + + @Test + fun `M return an empty byte array W copyOfRangeSafe() { out of bounds index }`( + @StringForgery(size = 32) data: String, + @IntForgery(min = 1, max = 512) positiveIndex: Int + ) { + // Given + val byteArray = data.toByteArray() + + // When + val result = byteArray.copyOfRangeSafe(0, byteArray.size + positiveIndex) + + // Then + assertThat(result).isEmpty() + } + + @Test + fun `M return an empty byte array W copyOfRangeSafe() { illegal index }`( + @StringForgery(size = 32) data: String + ) { + // Given + val byteArray = data.toByteArray() + + // When + val result = byteArray.copyOfRangeSafe(byteArray.lastIndex, 0) + + // Then + assertThat(result).isEmpty() + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ConcurrencyExtTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ConcurrencyExtTest.kt new file mode 100644 index 0000000000..202b7afb8d --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ConcurrencyExtTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ConcurrencyExtTest { + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Test + fun `M execute task W executeSafe()`( + @StringForgery name: String + ) { + // Given + val service: ExecutorService = mock() + val runnable: Runnable = mock() + doNothing().whenever(service).execute(runnable) + + // When + service.executeSafe(name, mockInternalLogger, runnable) + + // Then + verify(service).execute(runnable) + } + + @Test + fun `M not throw W executeSafe() {rejected exception}`( + @StringForgery name: String, + @StringForgery message: String + ) { + // Given + val service: ExecutorService = mock() + val runnable: Runnable = mock() + val exception = RejectedExecutionException(message) + doThrow(exception).whenever(service).execute(runnable) + + // When + service.executeSafe(name, mockInternalLogger, runnable) + + // Then + verify(service).execute(runnable) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule $name task on the executor", + exception + ) + } + + @Test + fun `M schedule task W scheduleSafe()`( + @StringForgery name: String, + @LongForgery delay: Long, + @Forgery unit: TimeUnit + ) { + // Given + val service: ScheduledExecutorService = mock() + val runnable: Runnable = mock() + val future: ScheduledFuture<*> = mock() + whenever(service.schedule(runnable, delay, unit)) doReturn future + + // When + val result: Any? = service.scheduleSafe(name, delay, unit, mockInternalLogger, runnable) + + // Then + assertThat(result).isSameAs(future) + verify(service).schedule(runnable, delay, unit) + } + + @Test + fun `M not throw W scheduleSafe() {rejected exception}`( + @StringForgery name: String, + @LongForgery delay: Long, + @Forgery unit: TimeUnit, + @StringForgery message: String + ) { + // Given + val service: ScheduledExecutorService = mock() + val runnable: Runnable = mock() + val exception = RejectedExecutionException(message) + doThrow(exception).whenever(service).schedule(runnable, delay, unit) + + // When + val result: Any? = service.scheduleSafe(name, delay, unit, mockInternalLogger, runnable) + + // Then + assertThat(result).isNull() + verify(service).schedule(runnable, delay, unit) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule $name task on the executor", + exception + ) + } + + @Test + fun `M submit task W submitSafe() {runnable} `( + @StringForgery name: String + ) { + // Given + val service: ExecutorService = mock() + val runnable: Runnable = mock() + val future: Future<*> = mock() + whenever(service.submit(runnable)) doReturn future + + // When + val result: Any? = service.submitSafe(name, mockInternalLogger, runnable) + + // Then + assertThat(result).isSameAs(future) + verify(service).submit(runnable) + } + + @Test + fun `M not throw W submitSafe() {runnable, rejected exception}`( + @StringForgery name: String, + @StringForgery message: String + ) { + // Given + val service: ExecutorService = mock() + val runnable: Runnable = mock() + val exception = RejectedExecutionException(message) + doThrow(exception).whenever(service).submit(runnable) + + // When + val result = service.submitSafe(name, mockInternalLogger, runnable) + + // Then + assertThat(result).isNull() + verify(service).submit(runnable) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule $name task on the executor", + exception + ) + } + + @Test + fun `M submit task W submitSafe() {callable} `( + @StringForgery name: String + ) { + // Given + val service: ExecutorService = mock() + val callable: Callable = mock() + val future: Future = mock() + whenever(service.submit(callable)) doReturn future + + // When + val result = service.submitSafe(name, mockInternalLogger, callable) + + // Then + assertThat(result).isSameAs(future) + verify(service).submit(callable) + } + + @Test + fun `M not throw W submitSafe() {callable, rejected exception}`( + @StringForgery name: String, + @StringForgery message: String + ) { + // Given + val service: ExecutorService = mock() + val callable: Callable = mock() + val exception = RejectedExecutionException(message) + doThrow(exception).whenever(service).submit(callable) + + // When + val result = service.submitSafe(name, mockInternalLogger, callable) + + // Then + assertThat(result).isNull() + verify(service).submit(callable) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + "Unable to schedule $name task on the executor", + exception + ) + } + + @Test + fun `M return result W getSafe()`( + @StringForgery operationName: String + ) { + // Given + val future = mock>() + val fakeResult = Any() + whenever(future.getSafe(operationName, mockInternalLogger)) doReturn fakeResult + + // When + val result = future.getSafe(operationName, mockInternalLogger) + + // Then + assertThat(result).isSameAs(fakeResult) + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M log error W getSafe() { exception thrown }`( + @StringForgery operationName: String, + forge: Forge + ) { + // Given + val future = mock>() + val fakeException = forge.anElementFrom( + ExecutionException(forge.aThrowable()), + CancellationException(), + InterruptedException() + ) + whenever(future.getSafe(operationName, mockInternalLogger)) doThrow fakeException + + // When + val result = future.getSafe(operationName, mockInternalLogger) + + // Then + assertThat(result).isNull() + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + message = "Unable to get result of the $operationName task", + throwable = fakeException + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtilsTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtilsTest.kt new file mode 100644 index 0000000000..d00c1ee8ee --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtilsTest.kt @@ -0,0 +1,319 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.utils.JsonSerializer.ITEM_SERIALIZATION_ERROR +import com.datadog.android.core.internal.utils.JsonSerializer.safeMapValuesToJson +import com.datadog.android.internal.utils.NULL_MAP_VALUE +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.forge.anException +import com.datadog.tools.unit.forge.exhaustiveAttributes +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.json.JSONArray +import org.json.JSONObject +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit +import kotlin.system.measureNanoTime + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +@MockitoSettings(strictness = Strictness.STRICT_STUBS) +internal class MiscUtilsTest { + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Test + fun `M repeat max N times W retryWithDelay { success = false }`(forge: Forge) { + // Given + val fakeTimes = forge.anInt(min = 1, max = 10) + val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) + val mockedBlock: () -> Boolean = mock() + whenever(mockedBlock.invoke()).thenReturn(false) + + // When + val wasSuccessful = retryWithDelay(mockedBlock, fakeTimes, fakeDelay, mockInternalLogger) + + // Then + assertThat(wasSuccessful).isFalse() + verify(mockedBlock, times(fakeTimes)).invoke() + } + + @Test + fun `M execute the block in a delayed loop W retryWithDelay`(forge: Forge) { + // Given + val fakeTimes = forge.anInt(min = 1, max = 4) + val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) + val mockedBlock: () -> Boolean = mock() + whenever(mockedBlock.invoke()).thenReturn(false) + + // When + val executionTime = measureNanoTime { retryWithDelay(mockedBlock, fakeTimes, fakeDelay, mockInternalLogger) } + + // Then + assertThat(executionTime).isCloseTo( + fakeTimes * fakeDelay, + Offset.offset(TimeUnit.SECONDS.toNanos(1)) + ) + } + + @Test + fun `M do nothing W retryWithDelay { times less or equal than 0 }`(forge: Forge) { + // Given + val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) + val mockedBlock: () -> Boolean = mock() + + // When + retryWithDelay(mockedBlock, forge.anInt(Int.MIN_VALUE, 1), fakeDelay, mockInternalLogger) + + // Then + verifyNoInteractions(mockedBlock) + } + + @Test + fun `M repeat until success W retryWithDelay { result false }`(forge: Forge) { + // Given + val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) + val mockedBlock: () -> Boolean = mock() + whenever(mockedBlock.invoke()).thenReturn(false).thenReturn(true) + + // When + val wasSuccessful = retryWithDelay(mockedBlock, 3, fakeDelay, mockInternalLogger) + + // Then + assertThat(wasSuccessful).isTrue() + verify(mockedBlock, times(2)).invoke() + } + + @Test + fun `M repeat until success W retryWithDelay { exception }`( + @Forgery exception: Exception, + forge: Forge + ) { + // Given + val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) + val mockedBlock: () -> Boolean = mock() + whenever(mockedBlock.invoke()).thenThrow(exception).thenReturn(true) + + // When + val wasSuccessful = retryWithDelay(mockedBlock, 3, fakeDelay, mockInternalLogger) + + // Then + assertThat(wasSuccessful).isTrue() + verify(mockedBlock, times(2)).invoke() + } + + @Test + fun `M provide the relevant JsonElement W toJsonElement { on Kotlin object }`(forge: Forge) { + // Given + val attributes = forge.exhaustiveAttributes().toMutableMap() + attributes[forge.aString()] = NULL_MAP_VALUE + attributes[forge.aString()] = JsonNull.INSTANCE + + // When + attributes.forEach { + // be careful here, we shouldn't pass `it`, because it has Map.Entry type, so will fall + // always into `else` branch of underlying assertion + val jsonElement = JsonSerializer.toJsonElement(it.value) + assertJsonElement(it.value, jsonElement) + } + } + + @Test + fun `M map values to JSON without throwing W safeMapValuesToJson()`(forge: Forge) { + // Given + val attributes = forge.exhaustiveAttributes().toMutableMap() + val fakeException = forge.anException() + val faultyKey = forge.anAlphabeticalString() + val faultyItem = object { + override fun toString(): String { + throw fakeException + } + } + val mockInternalLogger = mock() + + // When + val mapped = attributes.apply { this += faultyKey to faultyItem } + .safeMapValuesToJson(mockInternalLogger) + + // Then + assertThat(mapped).hasSize(attributes.size - 1) + assertThat(mapped.values).doesNotContainNull() + assertThat(mapped).doesNotContainKey(faultyKey) + + mockInternalLogger + .verifyLog( + level = InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + message = ITEM_SERIALIZATION_ERROR.format(Locale.US, faultyKey), + throwable = fakeException + ) + } + + @Test + fun `M return null W fromJsonElement() {JsonNull}`() { + // Given + val json = JsonNull.INSTANCE + + // When + val mapped = json.fromJsonElement() + + // Then + assertThat(mapped).isNull() + } + + @Test + fun `M return value W fromJsonElement() {JsonPrimitive boolean}`( + @BoolForgery value: Boolean + ) { + // Given + val json = JsonPrimitive(value) + + // When + val mapped = json.fromJsonElement() + + // Then + assertThat(mapped).isEqualTo(value) + } + + @Test + fun `M return value W fromJsonElement() {JsonPrimitive int}`( + @IntForgery value: Int + ) { + // Given + val json = JsonPrimitive(value) + + // When + val mapped = json.fromJsonElement() + + // Then + assertThat(mapped).isEqualTo(value) + } + + @Test + fun `M return value W fromJsonElement() {JsonPrimitive float}`( + @FloatForgery value: Float + ) { + // Given + val json = JsonPrimitive(value) + + // When + val mapped = json.fromJsonElement() + + // Then + assertThat(mapped).isEqualTo(value) + } + + @Test + fun `M return value W fromJsonElement() {JsonPrimitive String}`( + @StringForgery value: String + ) { + // Given + val json = JsonPrimitive(value) + + // When + val mapped = json.fromJsonElement() + + // Then + assertThat(mapped).isEqualTo(value) + } + + @Test + fun `M return value W fromJsonElement() {Json Object}`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHABETICAL)]), + value = AdvancedForgery(string = [StringForgery()]) + ) value: Map + ) { + // Given + val json = JsonObject() + value.forEach { (k, v) -> + json.addProperty(k, v) + } + + // When + val mapped = json.fromJsonElement() + + // Then + assertThat(mapped).isEqualTo(value) + } + + // region Internal + + private fun assertJsonElement(kotlinObject: Any?, jsonElement: JsonElement) { + when (kotlinObject) { + NULL_MAP_VALUE -> assertThat(jsonElement).isEqualTo(JsonNull.INSTANCE) + null -> assertThat(jsonElement).isEqualTo(JsonNull.INSTANCE) + JsonNull.INSTANCE -> assertThat(jsonElement).isEqualTo(JsonNull.INSTANCE) + is Boolean -> assertThat(jsonElement.asBoolean).isEqualTo(kotlinObject) + is Int -> assertThat(jsonElement.asInt).isEqualTo(kotlinObject) + is Long -> assertThat(jsonElement.asLong).isEqualTo(kotlinObject) + is Float -> assertThat(jsonElement.asFloat).isEqualTo(kotlinObject) + is Double -> assertThat(jsonElement.asDouble).isEqualTo(kotlinObject) + is String -> assertThat(jsonElement.asString).isEqualTo(kotlinObject) + is Date -> assertThat(jsonElement.asLong).isEqualTo(kotlinObject.time) + is JsonObject -> assertThat(jsonElement.asJsonObject).isEqualTo(kotlinObject) + is JsonArray -> assertThat(jsonElement.asJsonArray).isEqualTo(kotlinObject) + is Iterable<*> -> assertThat(jsonElement.asJsonArray).containsExactlyElementsOf( + kotlinObject.map { JsonSerializer.toJsonElement(it) } + ) + + is Map<*, *> -> assertThat(jsonElement.asJsonObject).satisfies { + assertThat(kotlinObject.keys.map { key -> key.toString() }) + .containsExactlyElementsOf(it.keySet()) + kotlinObject.entries.forEach { entry -> + assertJsonElement(entry.value, it[entry.key.toString()]) + } + } + + is JSONArray -> assertThat(jsonElement.asJsonArray.toString()) + .isEqualTo(kotlinObject.toString()) + + is JSONObject -> assertThat(jsonElement.asJsonObject.toString()) + .isEqualTo(kotlinObject.toString()) + + else -> assertThat(jsonElement.asString).isEqualTo(kotlinObject.toString()) + } + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ThreadExtTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ThreadExtTest.kt new file mode 100644 index 0000000000..44668cad7d --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/ThreadExtTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import com.datadog.android.internal.utils.asString +import com.datadog.android.internal.utils.loggableStackTrace +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +internal class ThreadExtTest { + + @RepeatedTest(16) + fun `M return name W Thread#State#asString()`( + @Forgery state: Thread.State + ) { + // When + assertThat(state.asString()).isEqualTo(state.name.lowercase()) + } + + @Test + fun `M return stack trace W loggableStackTrace()`() { + // Given + val stack = Thread.currentThread().stackTrace + + // When + val result = stack.loggableStackTrace() + + // Then + val lines = result.lines() + stack.forEachIndexed { i, frame -> + assertThat(lines[i]).contains(frame.toString()) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtilsTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtilsTest.kt new file mode 100644 index 0000000000..b8a107fb3e --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtilsTest.kt @@ -0,0 +1,143 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.utils + +import android.app.Application +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.impl.WorkManagerImpl +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.UploadWorker +import com.datadog.android.core.internal.CoreFeature +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.datadog.tools.unit.setStaticValue +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class WorkManagerUtilsTest { + + @Mock + lateinit var mockWorkManager: WorkManagerImpl + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @StringForgery + lateinit var fakeInstanceName: String + + @BeforeEach + fun `set up`() { + CoreFeature.disableKronosBackgroundSync = true + + whenever(mockWorkManager.cancelAllWorkByTag(anyString())) doReturn mock() + whenever( + mockWorkManager.enqueueUniqueWork( + anyString(), + any(), + any() + ) + ) doReturn mock() + } + + @AfterEach + fun `tear down`() { + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", null) + } + + @Test + fun `it will cancel the worker if WorkManager was correctly instantiated`() { + // Given + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) + + // When + cancelUploadWorker(appContext.mockInstance, fakeInstanceName, mockInternalLogger) + + // Then + verify(mockWorkManager).cancelAllWorkByTag("$TAG_DATADOG_UPLOAD/$fakeInstanceName") + } + + @Test + fun `it will handle the cancel exception if WorkManager was not correctly instantiated`() { + // When + cancelUploadWorker(appContext.mockInstance, fakeInstanceName, mockInternalLogger) + + // Then + verifyNoInteractions(mockWorkManager) + } + + @Test + fun `it will schedule the worker if WorkManager was correctly instantiated`() { + // Given + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) + + // When + triggerUploadWorker(appContext.mockInstance, fakeInstanceName, mockInternalLogger) + + // Then + argumentCaptor { + verify(mockWorkManager).enqueueUniqueWork( + eq(UPLOAD_WORKER_NAME), + eq(ExistingWorkPolicy.REPLACE), + capture() + ) + val workSpec = lastValue.workSpec + assertThat(workSpec.workerClassName).isEqualTo(UploadWorker::class.java.canonicalName) + assertThat(workSpec.input.getString(UploadWorker.DATADOG_INSTANCE_NAME)).isEqualTo(fakeInstanceName) + assertThat(lastValue.tags).contains("$TAG_DATADOG_UPLOAD/$fakeInstanceName") + } + } + + @Test + fun `it will handle the trigger exception if WorkManager was not correctly instantiated`() { + // When + triggerUploadWorker(appContext.mockInstance, fakeInstanceName, mockInternalLogger) + + // Then + verifyNoInteractions(mockWorkManager) + } + + companion object { + val appContext = ApplicationContextTestConfiguration(Application::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/sampling/DeterministicSamplerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/sampling/DeterministicSamplerTest.kt new file mode 100644 index 0000000000..75ff90434c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/sampling/DeterministicSamplerTest.kt @@ -0,0 +1,199 @@ +package com.datadog.android.core.sampling + +import com.datadog.android.utils.forge.Configurator +import com.datadog.trace.sampling.JavaDeterministicSampler +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.stream.Stream + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DeterministicSamplerTest { + + private lateinit var testedSampler: Sampler + + private var stubIdConverter: (ULong) -> ULong = { it } + + private lateinit var fakeTraceIds: List + + @Mock + lateinit var mockSampleRateProvider: () -> Float + + @BeforeEach + fun `set up`(forge: Forge) { + val listSize = forge.anInt(256, 1024) + fakeTraceIds = forge.aList(listSize) { aLong() } + testedSampler = DeterministicSampler( + stubIdConverter, + mockSampleRateProvider + ) + } + + @ParameterizedTest + @MethodSource("hardcodedFixtures") + fun `M return consistent results W sample() {hardcodedFixtures}`( + input: Fixture, + expectedDecision: Boolean + ) { + // Given + whenever(mockSampleRateProvider.invoke()) doReturn input.samplingRate + + // When + val sampled = testedSampler.sample(input.traceId) + + // + assertThat(sampled).isEqualTo(expectedDecision) + } + + @RepeatedTest(128) + fun `M return consistent results W sample() {java implementation}`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float + ) { + // Given + whenever(mockSampleRateProvider.invoke()) doReturn fakeSampleRate + val javaSampler = JavaDeterministicSampler(fakeSampleRate / 100f) + + // When + fakeTraceIds.forEach { + val result = testedSampler.sample(it.toULong()) + val expectedResult = javaSampler.sample(it) + + assertThat(result).isEqualTo(expectedResult) + } + } + + @RepeatedTest(128) + fun `the sampler will sample the values based on the fixed sample rate`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float + ) { + // Given + whenever(mockSampleRateProvider.invoke()) doReturn fakeSampleRate + var sampledIn = 0 + + // When + fakeTraceIds.forEach { + if (testedSampler.sample(it.toULong())) { + sampledIn++ + } + } + + // Then + val offset = 2.5f * fakeTraceIds.size + assertThat(sampledIn.toFloat()).isCloseTo(fakeTraceIds.size * fakeSampleRate / 100f, Offset.offset(offset)) + } + + @Test + fun `when sample rate is 0 all values will be dropped`() { + // Given + whenever(mockSampleRateProvider.invoke()) doReturn 0f + var sampledIn = 0 + + // When + fakeTraceIds.forEach { + if (testedSampler.sample(it.toULong())) { + sampledIn++ + } + } + + // Then + assertThat(sampledIn).isEqualTo(0) + } + + @Test + fun `when sample rate is 100 all values will pass`() { + // Given + whenever(mockSampleRateProvider.invoke()) doReturn 100f + var sampledIn = 0 + + // When + fakeTraceIds.forEach { + if (testedSampler.sample(it.toULong())) { + sampledIn++ + } + } + + // Then + assertThat(sampledIn).isEqualTo(fakeTraceIds.size) + } + + @Test + fun `when sample rate is below 0 it is normalized to 0`( + @FloatForgery(max = 0f) fakeSampleRate: Float + ) { + // Given + whenever(mockSampleRateProvider.invoke()) doReturn fakeSampleRate + + // When + val effectiveSampleRate = testedSampler.getSampleRate() + + // Then + assertThat(effectiveSampleRate).isZero + } + + @Test + fun `when sample rate is above 100 it is normalized to 100`( + @FloatForgery(min = 100.01f) fakeSampleRate: Float + ) { + // Given + whenever(mockSampleRateProvider.invoke()) doReturn fakeSampleRate + + // When + val effectiveSampleRate = testedSampler.getSampleRate() + + // Then + assertThat(effectiveSampleRate).isEqualTo(100f) + } + + /** + * A data class is necessary to wrap the ULong, otherwise the jvm runner + * converts it to Long at some point. + */ + data class Fixture( + val traceId: ULong, + val samplingRate: Float + ) + + companion object { + + // Those hardcoded values ensures we are consistent with the decisions of our + // Backend implementation of the knuth sampling method + @Suppress("unused") + @JvmStatic + fun hardcodedFixtures(): Stream { + return listOf( + Arguments.of(Fixture(4815162342u, 55.9f), false), + Arguments.of(Fixture(4815162342u, 56.0f), true), + Arguments.of(Fixture(1415926535897932384u, 90.5f), false), + Arguments.of(Fixture(1415926535897932384u, 90.6f), true), + Arguments.of(Fixture(718281828459045235u, 7.4f), false), + Arguments.of(Fixture(718281828459045235u, 7.5f), true), + Arguments.of(Fixture(41421356237309504u, 32.1f), false), + Arguments.of(Fixture(41421356237309504u, 32.2f), true), + Arguments.of(Fixture(6180339887498948482u, 68.2f), false), + Arguments.of(Fixture(6180339887498948482u, 68.3f), true) + ).stream() + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/sampling/RateBasedSamplerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/sampling/RateBasedSamplerTest.kt new file mode 100644 index 0000000000..0639945d34 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/sampling/RateBasedSamplerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.sampling + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import kotlin.math.pow +import kotlin.math.sqrt + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class RateBasedSamplerTest { + + private lateinit var testedSampler: RateBasedSampler + + @FloatForgery(min = 0f, max = 100f) + var randomSampleRate: Float = 0.0f + + @BeforeEach + fun `set up`() { + testedSampler = RateBasedSampler(randomSampleRate) + } + + @Test + fun `the sampler will sample the values based on the fixed sample rate`() { + // Given + val dataSize = 1000 + val testRepeats = 100 + val computedSampleRates = mutableListOf() + + // When + repeat(testRepeats) { + var validated = 0 + repeat(dataSize) { + val isValid = if (testedSampler.sample(Unit)) 1 else 0 + validated += isValid + } + val computedSampleRate = (validated.toDouble() / dataSize.toDouble()) * 100 + computedSampleRates.add(computedSampleRate) + } + val sampleRateMean = computedSampleRates.sum().div(computedSampleRates.size) + val variance = computedSampleRates + .sumOf { (sampleRateMean.minus(it)).pow(2) } + .div(computedSampleRates.size) + val deviation = sqrt(variance) + + // Then + assertThat(sampleRateMean).isCloseTo( + randomSampleRate.toDouble(), + Offset.offset(deviation) + ) + } + + @Test + fun `the sampler will sample the values based on the dynamic sample rate`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRateA: Float, + @FloatForgery(min = 0f, max = 100f) fakeSampleRateB: Float + ) { + // Given + val dataSize = 1000 + val testRepeats = 100 + val computedSampleRates = mutableListOf() + var invocationCounter = 0 + testedSampler = RateBasedSampler { + invocationCounter++ + if (invocationCounter.mod(dataSize) <= dataSize / 2) { + fakeSampleRateA + } else { + fakeSampleRateB + } + } + + // When + repeat(testRepeats) { + var validated = 0 + repeat(dataSize) { + val isValid = if (testedSampler.sample(Unit)) 1 else 0 + validated += isValid + } + val computedSampleRate = (validated.toDouble() / dataSize.toDouble()) * 100 + computedSampleRates.add(computedSampleRate) + } + val sampleRateMean = computedSampleRates.sum().div(computedSampleRates.size) + val variance = computedSampleRates + .sumOf { (sampleRateMean.minus(it)).pow(2) } + .div(computedSampleRates.size) + val deviation = sqrt(variance) + + // Then + assertThat(sampleRateMean).isCloseTo( + (fakeSampleRateA + fakeSampleRateB).toDouble() / 2, + Offset.offset(deviation) + ) + } + + @Test + fun `when sample rate is 0 all values will be dropped`() { + testedSampler = RateBasedSampler(0.0f) + + var validated = 0 + val dataSize = 10 + + repeat(dataSize) { + val isValid = if (testedSampler.sample(Unit)) 1 else 0 + validated += isValid + } + + assertThat(validated).isEqualTo(0) + } + + @Test + fun `when sample rate is 100 all values will pass`() { + testedSampler = RateBasedSampler(100.0f) + + var validated = 0 + val dataSize = 10 + + repeat(dataSize) { + val isValid = if (testedSampler.sample(Unit)) 1 else 0 + validated += isValid + } + + assertThat(validated).isEqualTo(dataSize) + } + + @Test + fun `when sample rate is below 0 it is normalized to 0`( + @FloatForgery(max = 0f) fakeSampleRate: Float + ) { + // Given + testedSampler = RateBasedSampler(fakeSampleRate) + + // When + val effectiveSampleRate = testedSampler.getSampleRate() + + // Then + assertThat(effectiveSampleRate).isZero + } + + @Test + fun `when sample rate is above 100 it is normalized to 100`( + @FloatForgery(min = 100.01f) fakeSampleRate: Float + ) { + // Given + testedSampler = RateBasedSampler(fakeSampleRate) + + // When + val effectiveSampleRate = testedSampler.getSampleRate() + + // Then + assertThat(effectiveSampleRate).isEqualTo(100f) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/serializer/GsonCompatibilityTests.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/serializer/GsonCompatibilityTests.kt new file mode 100644 index 0000000000..685a6d7be3 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/serializer/GsonCompatibilityTests.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.serializer + +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions + +/** + * This class is used to test the compatibility of the Gson serializer with our current `.` + * keys in the JSON payloads. This should make sure that Json properties are added into the + * `toString` representation in the order they were added in the JSON object. + */ + +@Extensions(ExtendWith(ForgeExtension::class)) +internal class GsonCompatibilityTests { + + @RepeatedTest(5) + fun `M add the properties in order in the string W toString()`(forge: Forge) { + val propertiesKeysValues = forge.aMap(size = forge.anInt(min = 5, max = 50)) { + val key = forge.aList(size = forge.anInt(min = 1, max = 10)) { + anAlphabeticalString() + }.joinToString(".") + val values = listOf( + anInt(), + aBool(), + aNumericalString(), + anHexadecimalString(), + anAlphabeticalString() + ) + val value = forge.anElementFrom(values) + + key to value + } + val expectedJsonRepresentation = buildString { + append("{") + propertiesKeysValues.entries.forEachIndexed { index, entry -> + when (entry.value) { + is Int, is Boolean -> append("\"${entry.key}\":${entry.value}") + else -> append("\"${entry.key}\":\"${entry.value}\"") + } + if (index < propertiesKeysValues.size - 1) { + append(",") + } + } + append("}") + } + val jsonObject = JsonObject().apply { + propertiesKeysValues.forEach { (key, value) -> + when (value) { + is Int -> addProperty(key, value) + is Boolean -> addProperty(key, value) + is String -> addProperty(key, value) + } + } + } + + // When + val actualJsonRepresentation = jsonObject.toString() + + // Then + assertThat(actualJsonRepresentation).isEqualTo(expectedJsonRepresentation) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/CrashReportsFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/CrashReportsFeatureTest.kt new file mode 100644 index 0000000000..23dd516eae --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/CrashReportsFeatureTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.error.internal + +import android.app.Application +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class CrashReportsFeatureTest { + + private lateinit var testedFeature: CrashReportsFeature + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + var jvmExceptionHandler: Thread.UncaughtExceptionHandler? = null + + @BeforeEach + fun `set up crash reports`() { + testedFeature = CrashReportsFeature(mockSdkCore) + jvmExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + } + + @AfterEach + fun `tear down crash reports`() { + Thread.setDefaultUncaughtExceptionHandler(jvmExceptionHandler) + testedFeature.originalUncaughtExceptionHandler = jvmExceptionHandler + } + + @Test + fun `M register crash handler W initialize`() { + // When + testedFeature.onInitialize(appContext.mockInstance) + + // Then + val handler = Thread.getDefaultUncaughtExceptionHandler() + assertThat(handler) + .isInstanceOf(DatadogExceptionHandler::class.java) + } + + @Test + fun `M restore original crash handler W onStop()`() { + // Given + val mockOriginalHandler: Thread.UncaughtExceptionHandler = mock() + Thread.setDefaultUncaughtExceptionHandler(mockOriginalHandler) + + // When + testedFeature.onInitialize(appContext.mockInstance) + testedFeature.onStop() + + // Then + val finalHandler = Thread.getDefaultUncaughtExceptionHandler() + assertThat(finalHandler).isSameAs(mockOriginalHandler) + } + + companion object { + val appContext = ApplicationContextTestConfiguration(Application::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt new file mode 100644 index 0000000000..5450f205ca --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.error.internal + +import android.content.Context +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.impl.WorkManagerImpl +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.UploadWorker +import com.datadog.android.core.feature.event.JvmCrash +import com.datadog.android.core.internal.CoreFeature +import com.datadog.android.core.internal.thread.waitToIdle +import com.datadog.android.core.internal.utils.TAG_DATADOG_UPLOAD +import com.datadog.android.core.internal.utils.UPLOAD_WORKER_NAME +import com.datadog.android.internal.utils.loggableStackTrace +import com.datadog.android.utils.config.ApplicationContextTestConfiguration +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.datadog.tools.unit.setStaticValue +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.FileNotFoundException +import java.io.IOException +import java.util.concurrent.ThreadPoolExecutor + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogExceptionHandlerTest { + + private var originalHandler: Thread.UncaughtExceptionHandler? = null + + lateinit var testedHandler: DatadogExceptionHandler + + @Mock + lateinit var mockPreviousHandler: Thread.UncaughtExceptionHandler + + @Mock + lateinit var mockSdkCore: InternalSdkCore + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockLogsFeatureScope: FeatureScope + + @Mock + lateinit var mockRumFeatureScope: FeatureScope + + @Mock + lateinit var mockWorkManager: WorkManagerImpl + + @Forgery + lateinit var fakeThrowable: Throwable + + @StringForgery + lateinit var fakeInstanceName: String + + @BeforeEach + fun `set up`() { + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn mockLogsFeatureScope + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.name) doReturn fakeInstanceName + + CoreFeature.disableKronosBackgroundSync = true + + originalHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler(mockPreviousHandler) + testedHandler = DatadogExceptionHandler( + sdkCore = mockSdkCore, + appContext = appContext.mockInstance + ) + testedHandler.register() + } + + @AfterEach + fun `tear down`() { + Thread.setDefaultUncaughtExceptionHandler(originalHandler) + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", null) + Datadog.stopInstance() + } + + @Test + fun `M do not send exception to logs W uncaughtException() {no previous handler}`() { + // Given + Thread.setDefaultUncaughtExceptionHandler(null) + testedHandler.register() + val currentThread = Thread.currentThread() + + // When + testedHandler.uncaughtException(currentThread, fakeThrowable) + + // Then + verifyNoInteractions(mockLogsFeatureScope) + } + + @Test + fun `M do not send exception to logs W uncaughtException() {previous handler}`() { + // Given + val currentThread = Thread.currentThread() + + // When + testedHandler.uncaughtException(currentThread, fakeThrowable) + + // Then + verifyNoInteractions(mockLogsFeatureScope) + verify(mockPreviousHandler).uncaughtException(currentThread, fakeThrowable) + } + + // endregion + + @Test + fun `M wait for the executor to idle W exception caught`() { + // Given + val mockScheduledThreadExecutor: ThreadPoolExecutor = mock() + whenever(mockSdkCore.getPersistenceExecutorService()) doReturn mockScheduledThreadExecutor + Thread.setDefaultUncaughtExceptionHandler(null) + testedHandler.register() + val currentThread = Thread.currentThread() + + // When + testedHandler.uncaughtException(currentThread, fakeThrowable) + + // Then + verify(mockScheduledThreadExecutor) + .waitToIdle(DatadogExceptionHandler.MAX_WAIT_FOR_IDLE_TIME_IN_MS, mockInternalLogger) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogExceptionHandler.EXECUTOR_NOT_IDLED_WARNING_MESSAGE, + mode = never() + ) + } + + @Test + fun `M log warning message W exception caught { executor could not be idled }`() { + // Given + val mockScheduledThreadExecutor: ThreadPoolExecutor = mock { + whenever(it.taskCount).thenReturn(2) + whenever(it.completedTaskCount).thenReturn(0) + } + whenever(mockSdkCore.getPersistenceExecutorService()) doReturn mockScheduledThreadExecutor + Thread.setDefaultUncaughtExceptionHandler(null) + testedHandler.register() + val currentThread = Thread.currentThread() + + // When + testedHandler.uncaughtException(currentThread, fakeThrowable) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogExceptionHandler.EXECUTOR_NOT_IDLED_WARNING_MESSAGE + ) + } + + @Test + fun `M schedule the worker W logging an exception`() { + // Given + whenever( + mockWorkManager.enqueueUniqueWork( + ArgumentMatchers.anyString(), + any(), + any() + ) + ) doReturn mock() + + WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) + Thread.setDefaultUncaughtExceptionHandler(null) + testedHandler.register() + val currentThread = Thread.currentThread() + + // When + testedHandler.uncaughtException(currentThread, fakeThrowable) + + // Then + argumentCaptor { + verify(mockWorkManager).enqueueUniqueWork( + eq(UPLOAD_WORKER_NAME), + eq(ExistingWorkPolicy.REPLACE), + capture() + ) + val workSpec = lastValue.workSpec + assertThat(workSpec.workerClassName).isEqualTo(UploadWorker::class.java.canonicalName) + assertThat(workSpec.input.getString(UploadWorker.DATADOG_INSTANCE_NAME)).isEqualTo(fakeInstanceName) + assertThat(lastValue.tags).contains("$TAG_DATADOG_UPLOAD/$fakeInstanceName") + } + } + + // region Forward to RUM + + @Test + fun `M log dev info W caught exception { no RUM feature registered }`() { + // Given + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + Thread.setDefaultUncaughtExceptionHandler(null) + testedHandler.register() + val currentThread = Thread.currentThread() + + // When + testedHandler.uncaughtException(currentThread, fakeThrowable) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + DatadogExceptionHandler.MISSING_RUM_FEATURE_INFO + ) + } + + @Test + fun `M register RUM Error W RUM feature is registered { exception with message }`() { + // Given + val currentThread = Thread.currentThread() + + // When + testedHandler.uncaughtException(currentThread, fakeThrowable) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + + assertThat(lastValue).isInstanceOf(JvmCrash.Rum::class.java) + + val crashEvent = lastValue as JvmCrash.Rum + + assertThat(crashEvent.throwable).isSameAs(fakeThrowable) + assertThat(crashEvent.message).isEqualTo(fakeThrowable.message) + with(crashEvent.threads) { + assertThat(this).isNotEmpty + assertThat(filter { it.crashed }).hasSize(1) + val crashedThread = first { it.crashed } + assertThat(crashedThread.name).isEqualTo(currentThread.name) + assertThat(crashedThread.stack).isEqualTo(fakeThrowable.loggableStackTrace()) + val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name } + assertThat(nonCrashedThreadNames).isNotEmpty + assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name) + } + } + verify(mockPreviousHandler).uncaughtException(currentThread, fakeThrowable) + } + + @RepeatedTest(2) + fun `M register RUM Error W RUM feature is registered { exception without message }`( + forge: Forge + ) { + // Given + val currentThread = Thread.currentThread() + val throwable = forge.aThrowableWithoutMessage() + + // When + testedHandler.uncaughtException(currentThread, throwable) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + + assertThat(lastValue).isInstanceOf(JvmCrash.Rum::class.java) + + val crashEvent = lastValue as JvmCrash.Rum + + assertThat(crashEvent.throwable).isSameAs(throwable) + assertThat(crashEvent.message) + .isEqualTo("Application crash detected: ${throwable.javaClass.canonicalName}") + with(crashEvent.threads) { + assertThat(this).isNotEmpty + assertThat(filter { it.crashed }).hasSize(1) + val crashedThread = first { it.crashed } + assertThat(crashedThread.name).isEqualTo(currentThread.name) + assertThat(crashedThread.stack).isEqualTo(throwable.loggableStackTrace()) + val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name } + assertThat(nonCrashedThreadNames).isNotEmpty + assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name) + } + } + verify(mockPreviousHandler).uncaughtException(currentThread, throwable) + } + + @Test + fun `M register RUM Error W RUM feature is registered { exception without message or class }`() { + // Given + val currentThread = Thread.currentThread() + val throwable = object : RuntimeException() {} + + // When + testedHandler.uncaughtException(currentThread, throwable) + + // Then + argumentCaptor { + verify(mockRumFeatureScope).sendEvent(capture()) + + assertThat(lastValue).isInstanceOf(JvmCrash.Rum::class.java) + + val crashEvent = lastValue as JvmCrash.Rum + + assertThat(crashEvent.throwable).isSameAs(throwable) + assertThat(crashEvent.message) + .isEqualTo("Application crash detected: ${throwable.javaClass.simpleName}") + with(crashEvent.threads) { + assertThat(this).isNotEmpty + assertThat(filter { it.crashed }).hasSize(1) + val crashedThread = first { it.crashed } + assertThat(crashedThread.name).isEqualTo(currentThread.name) + assertThat(crashedThread.stack).isEqualTo(throwable.loggableStackTrace()) + val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name } + assertThat(nonCrashedThreadNames).isNotEmpty + assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name) + } + } + verify(mockPreviousHandler).uncaughtException(currentThread, throwable) + } + + // endregion + + private fun Forge.aThrowableWithoutMessage(): Throwable { + val exceptionClass = anElementFrom( + IOException::class.java, + IllegalStateException::class.java, + UnknownError::class.java, + ArrayIndexOutOfBoundsException::class.java, + NullPointerException::class.java, + UnsupportedOperationException::class.java, + FileNotFoundException::class.java + ) + + return if (aBool()) { + exceptionClass.getDeclaredConstructor().newInstance() + } else { + exceptionClass.constructors + .first { + it.parameterTypes.size == 1 && it.parameterTypes[0] == String::class.java + } + .newInstance(anElementFrom("", aWhitespaceString())) as Throwable + } + } + + companion object { + val appContext = ApplicationContextTestConfiguration(Context::class.java) + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(appContext) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/event/MapperSerializerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/event/MapperSerializerTest.kt new file mode 100644 index 0000000000..4803fca02c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/event/MapperSerializerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.event + +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class MapperSerializerTest { + + lateinit var testedMapperSerializer: MapperSerializer + + @Mock + lateinit var mockMapper: EventMapper + + @Mock + lateinit var mockSerializer: Serializer + + @BeforeEach + fun `set up`() { + testedMapperSerializer = MapperSerializer(mockMapper, mockSerializer) + } + + @Test + fun `M return null W serialize() {mapper returns null}`( + @StringForgery input: String + ) { + // Given + whenever(mockMapper.map(input)) doReturn null + + // When + val result = testedMapperSerializer.serialize(input) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W serialize() {serializer returns null}`( + @StringForgery input: String, + @StringForgery mapped: String + ) { + // Given + whenever(mockMapper.map(input)) doReturn mapped + whenever(mockSerializer.serialize(mapped)) doReturn null + + // When + val result = testedMapperSerializer.serialize(input) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return mapped then serialized data W serialize()`( + @StringForgery input: String, + @StringForgery mapped: String, + @StringForgery serialized: String + ) { + // Given + whenever(mockMapper.map(input)) doReturn mapped + whenever(mockSerializer.serialize(mapped)) doReturn serialized + + // When + val result = testedMapperSerializer.serialize(input) + + // Then + assertThat(result).isEqualTo(serialized) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandlerTest.kt new file mode 100644 index 0000000000..1b3d6c071c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandlerTest.kt @@ -0,0 +1,390 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.file.FileReader +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.firstValue +import org.mockito.kotlin.lastValue +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.util.Locale +import java.util.UUID +import java.util.concurrent.ExecutorService + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogNdkCrashHandlerTest { + + private lateinit var testedHandler: DatadogNdkCrashHandler + + @Mock + lateinit var mockExecutorService: ExecutorService + + @Mock + lateinit var mockNdkCrashLogDeserializer: Deserializer + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + @Mock + lateinit var mockLogsFeatureScope: FeatureScope + + @Mock + lateinit var mockRumFeatureScope: FeatureScope + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockEnvFileReader: FileReader + + @Mock + lateinit var mockLastRumViewEventProvider: () -> JsonObject? + + lateinit var fakeNdkCacheDir: File + + @Forgery + lateinit var fakeNdkCrashLog: NdkCrashLog + + @Captor + lateinit var captureRunnable: ArgumentCaptor + + @TempDir + lateinit var tempDir: File + + @BeforeEach + fun `set up`() { + fakeNdkCacheDir = File(tempDir, DatadogNdkCrashHandler.NDK_CRASH_REPORTS_FOLDER_NAME) + whenever(mockEnvFileReader.readData(any())) doAnswer { + it.getArgument(0).readBytes() + } + + whenever( + mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME) + ) doReturn mockLogsFeatureScope + + whenever( + mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME) + ) doReturn mockRumFeatureScope + + testedHandler = DatadogNdkCrashHandler( + tempDir, + mockExecutorService, + mockNdkCrashLogDeserializer, + mockInternalLogger, + mockLastRumViewEventProvider + ) + } + + // region prepareData + + @Test + fun `M read crash data W prepareData()`( + @StringForgery crashDataStr: String + ) { + // Given + fakeNdkCacheDir.mkdirs() + File(fakeNdkCacheDir, DatadogNdkCrashHandler.CRASH_DATA_FILE_NAME).writeText(crashDataStr) + whenever(mockNdkCrashLogDeserializer.deserialize(crashDataStr)) doReturn fakeNdkCrashLog + + // When + testedHandler.prepareData() + + // Then + assertThat(testedHandler.lastNdkCrashLog).isNull() + verify(mockExecutorService).execute(captureRunnable.capture()) + captureRunnable.firstValue.run() + assertThat(testedHandler.lastNdkCrashLog) + .isEqualTo(fakeNdkCrashLog) + } + + @Test + fun `M read last RUM View event W prepareData()`( + forge: Forge + ) { + // Given + fakeNdkCacheDir.mkdirs() + val fakeViewEvent = forge.aFakeViewEvent() + whenever(mockLastRumViewEventProvider()) doReturn fakeViewEvent.toJson() + + // When + testedHandler.prepareData() + + // Then + assertThat(testedHandler.lastRumViewEvent).isNull() + verify(mockExecutorService).execute(captureRunnable.capture()) + captureRunnable.firstValue.run() + assertThat(testedHandler.lastRumViewEvent) + .isEqualTo(fakeViewEvent.toJson()) + } + + @Test + fun `M do nothing W prepareData() {directory does not exist}`() { + // When + testedHandler.prepareData() + whenever(mockNdkCrashLogDeserializer.deserialize(any())) doReturn mock() + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + captureRunnable.firstValue.run() + assertThat(testedHandler.lastRumViewEvent).isNull() + assertThat(testedHandler.lastNdkCrashLog).isNull() + } + + @Test + fun `M clear crash data W prepareData()`( + @StringForgery crashData: String, + @StringForgery networkInfo: String, + @StringForgery userInfo: String + ) { + // Given + fakeNdkCacheDir.mkdirs() + + File(fakeNdkCacheDir, DatadogNdkCrashHandler.CRASH_DATA_FILE_NAME).writeText(crashData) + File(fakeNdkCacheDir, DatadogNdkCrashHandler.NETWORK_INFO_FILE_NAME) + .writeText(networkInfo) + File(fakeNdkCacheDir, DatadogNdkCrashHandler.USER_INFO_FILE_NAME).writeText(userInfo) + + // When + testedHandler.prepareData() + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + captureRunnable.firstValue.run() + assertThat(fakeNdkCacheDir).isEmptyDirectory + } + + // endregion + + // region handleNdkCrash / Logs + + @Test + fun `M do nothing W handleNdkCrash() {no crash data}`() { + // When + testedHandler.handleNdkCrash(mockSdkCore) + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + captureRunnable.firstValue.run() + verifyNoInteractions(mockSdkCore) + } + + @Test + fun `M not send log W handleNdkCrash()`() { + // Given + testedHandler.lastNdkCrashLog = fakeNdkCrashLog + + // When + testedHandler.handleNdkCrash(mockSdkCore) + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + verifyNoInteractions(mockSdkCore) + captureRunnable.firstValue.run() + verifyNoInteractions(mockLogsFeatureScope, mockInternalLogger) + } + + // endregion + + // region handleNdkCrash / RUM + + @Test + fun `M not send RUM event W handleNdkCrash() { RUM feature is not registered }`( + forge: Forge + ) { + // Given + val fakeViewEvent = forge.aFakeViewEvent() + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + testedHandler.lastNdkCrashLog = fakeNdkCrashLog + testedHandler.lastRumViewEvent = fakeViewEvent.toJson() + + // When + testedHandler.handleNdkCrash(mockSdkCore) + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + verifyNoInteractions(mockSdkCore) + captureRunnable.firstValue.run() + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + DatadogNdkCrashHandler.INFO_RUM_FEATURE_NOT_REGISTERED + ) + } + + @Test + fun `M not send RUM event W handleNdkCrash() { missing last RUM view event }`() { + // Given + testedHandler.lastNdkCrashLog = fakeNdkCrashLog + + // When + testedHandler.handleNdkCrash(mockSdkCore) + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + verifyNoInteractions(mockSdkCore) + captureRunnable.firstValue.run() + verifyNoInteractions(mockInternalLogger, mockRumFeatureScope) + } + + @Test + fun `M send RUM event W handleNdkCrash()`(forge: Forge) { + // Given + val fakeViewEvent = forge.aFakeViewEvent() + testedHandler.lastNdkCrashLog = fakeNdkCrashLog + testedHandler.lastRumViewEvent = fakeViewEvent.toJson() + + // When + testedHandler.handleNdkCrash(mockSdkCore) + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + verifyNoInteractions(mockSdkCore) + captureRunnable.firstValue.run() + verify(mockRumFeatureScope).sendEvent( + mapOf( + "type" to "ndk_crash", + "sourceType" to "ndk", + "timestamp" to fakeNdkCrashLog.timestamp, + "timeSinceAppStartMs" to fakeNdkCrashLog.timeSinceAppStartMs, + "signalName" to fakeNdkCrashLog.signalName, + "stacktrace" to fakeNdkCrashLog.stacktrace, + "message" to DatadogNdkCrashHandler.LOG_CRASH_MSG + .format(Locale.US, fakeNdkCrashLog.signalName), + "lastViewEvent" to fakeViewEvent.toJson() + ) + ) + } + + @Test + fun `M send RUM event W handleNdkCrash() { override native source type } `(forge: Forge) { + // Given + val handler = DatadogNdkCrashHandler( + tempDir, + mockExecutorService, + mockNdkCrashLogDeserializer, + mockInternalLogger, + lastRumViewEventProvider = { JsonObject() }, + nativeCrashSourceType = "ndk+il2cpp" + ) + + val fakeViewEvent = forge.aFakeViewEvent() + handler.lastNdkCrashLog = fakeNdkCrashLog + handler.lastRumViewEvent = fakeViewEvent.toJson() + + // When + handler.handleNdkCrash(mockSdkCore) + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + verifyNoInteractions(mockSdkCore) + captureRunnable.firstValue.run() + verify(mockRumFeatureScope).sendEvent( + mapOf( + "type" to "ndk_crash", + "sourceType" to "ndk+il2cpp", + "timestamp" to fakeNdkCrashLog.timestamp, + "timeSinceAppStartMs" to fakeNdkCrashLog.timeSinceAppStartMs, + "signalName" to fakeNdkCrashLog.signalName, + "stacktrace" to fakeNdkCrashLog.stacktrace, + "message" to DatadogNdkCrashHandler.LOG_CRASH_MSG + .format(Locale.US, fakeNdkCrashLog.signalName), + "lastViewEvent" to fakeViewEvent.toJson() + ) + ) + } + + @Test + fun `M clear the references W handleNdkCrash()`( + forge: Forge + ) { + // Given + val fakeViewEvent = forge.aFakeViewEvent() + testedHandler.lastNdkCrashLog = fakeNdkCrashLog + testedHandler.lastRumViewEvent = fakeViewEvent.toJson() + + // When + testedHandler.handleNdkCrash(mockSdkCore) + + // Then + verify(mockExecutorService).execute(captureRunnable.capture()) + verifyNoInteractions(mockSdkCore) + captureRunnable.lastValue.run() + + assertThat(testedHandler.lastNdkCrashLog).isNull() + assertThat(testedHandler.lastRumViewEvent).isNull() + } + + // endregion + + // region Internal + + // TODO RUMM-0000 We don't have an access to RUM models from core. So we fake the class. + // Ideally it would be nice to have the real model (maybe generate the one for the test + // runtime classpath only somehow) + private class ViewEvent(val applicationId: String, val sessionId: String, val viewId: String) { + fun toJson(): JsonObject { + return JsonObject().apply { + add( + "application", + JsonObject().apply { this.addProperty("id", applicationId) } + ) + add( + "session", + JsonObject().apply { this.addProperty("id", sessionId) } + ) + add( + "view", + JsonObject().apply { this.addProperty("id", viewId) } + ) + } + } + } + + private fun Forge.aFakeViewEvent(): ViewEvent = ViewEvent( + applicationId = getForgery().toString(), + sessionId = getForgery().toString(), + viewId = getForgery().toString() + ) + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashLogDeserializerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashLogDeserializerTest.kt new file mode 100644 index 0000000000..e1c66ffa17 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashLogDeserializerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class NdkCrashLogDeserializerTest { + + private lateinit var testedDeserializer: NdkCrashLogDeserializer + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedDeserializer = NdkCrashLogDeserializer(mockInternalLogger) + } + + @Test + fun `M deserialize a model W deserialize`( + @Forgery fakeNdkCrashLog: NdkCrashLog + ) { + // GIVEN + val serializedNdkCrashLog = fakeNdkCrashLog.toJson() + + // WHEN + val deserializedNdkCrashLog = testedDeserializer.deserialize(serializedNdkCrashLog) + + // THEN + assertThat(deserializedNdkCrashLog).isEqualTo(fakeNdkCrashLog) + } + + @Test + fun `M return null W deserialize { wrong Json format }`() { + // WHEN + val deserializedEvent = testedDeserializer.deserialize("{]}") + + // THEN + assertThat(deserializedEvent).isNull() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashLogTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashLogTest.kt new file mode 100644 index 0000000000..a36156dd09 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashLogTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class NdkCrashLogTest { + + @Test + fun `M deserialize an NdkCrashLog W required`(@Forgery fakeNdkCrashLog: NdkCrashLog) { + // GIVEN + val serializedLog = fakeNdkCrashLog.toJson() + + // WHEN + val deserializedLog: NdkCrashLog = NdkCrashLog.fromJson(serializedLog) + + // THEN + assertThat(deserializedLog).isEqualTo(fakeNdkCrashLog) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/test/elmyr/PersistenceStrategyBatchForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/test/elmyr/PersistenceStrategyBatchForgeryFactory.kt new file mode 100644 index 0000000000..ea5ea4b2fb --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/test/elmyr/PersistenceStrategyBatchForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.test.elmyr + +import com.datadog.android.core.persistence.PersistenceStrategy +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class PersistenceStrategyBatchForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): PersistenceStrategy.Batch { + return PersistenceStrategy.Batch( + batchId = forge.anAlphabeticalString(), + metadata = forge.aNullable { aString().toByteArray() }, + events = forge.aList { getForgery() } + ) + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/utils/CharExt.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/CharExt.kt similarity index 100% rename from dd-sdk-android/src/test/kotlin/com/datadog/android/utils/CharExt.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/CharExt.kt diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/InternalLoggerUtils.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/InternalLoggerUtils.kt new file mode 100644 index 0000000000..181c7c2c84 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/InternalLoggerUtils.kt @@ -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 2016-Present Datadog, Inc. + */ + +@file:Suppress("MethodOverloading") + +package com.datadog.android.utils + +import com.datadog.android.api.InternalLogger +import org.assertj.core.api.Assertions.assertThat +import org.mockito.ArgumentMatchers.isA +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.verification.VerificationMode + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + target: InternalLogger.Target, + message: String, + throwable: Throwable? = null, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(target), + capture(), + same(throwable), + eq(onlyOnce), + eq(additionalProperties) + ) + allValues.forEach { + assertThat(it()).isEqualTo(message) + } + } +} + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + target: InternalLogger.Target, + message: String, + throwableClass: Class, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(target), + capture(), + isA(throwableClass), + eq(onlyOnce), + eq(additionalProperties) + ) + allValues.forEach { + assertThat(it()).isEqualTo(message) + } + } +} + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + targets: List, + message: String?, + throwable: Throwable? = null, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(targets), + capture(), + same(throwable), + eq(onlyOnce), + eq(additionalProperties) + ) + assertThat(firstValue()).isEqualTo(message) + } +} + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + targets: List, + message: String?, + throwableClass: Class, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(targets), + capture(), + isA(throwableClass), + eq(onlyOnce), + eq(additionalProperties) + ) + assertThat(firstValue()).isEqualTo(message) + } +} + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + target: InternalLogger.Target, + messageMatcher: (String) -> Boolean, + throwable: Throwable? = null, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(target), + capture(), + same(throwable), + eq(onlyOnce), + eq(additionalProperties) + ) + allValues.forEach { + assertThat(firstValue()).matches(messageMatcher) + } + } +} + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + target: InternalLogger.Target, + messageMatcher: (String) -> Boolean, + throwableClass: Class, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(target), + capture(), + isA(throwableClass), + eq(onlyOnce), + eq(additionalProperties) + ) + allValues.forEach { + assertThat(firstValue()).matches(messageMatcher) + } + } +} + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + targets: List, + messageMatcher: (String) -> Boolean, + throwable: Throwable? = null, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(targets), + capture(), + same(throwable), + eq(onlyOnce), + eq(additionalProperties) + ) + assertThat(firstValue()).matches(messageMatcher) + } +} + +fun InternalLogger.verifyLog( + level: InternalLogger.Level, + targets: List, + messageMatcher: (String) -> Boolean, + throwableClass: Class, + onlyOnce: Boolean = false, + mode: VerificationMode = times(1), + additionalProperties: Map? = null +) { + argumentCaptor<() -> String> { + verify(this@verifyLog, mode).log( + eq(level), + eq(targets), + capture(), + isA(throwableClass), + eq(onlyOnce), + eq(additionalProperties) + ) + assertThat(firstValue()).matches(messageMatcher) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/JointToStringVsStringBuilderPerformanceTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/JointToStringVsStringBuilderPerformanceTest.kt new file mode 100644 index 0000000000..6f3b8acd40 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/JointToStringVsStringBuilderPerformanceTest.kt @@ -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 2016-Present Datadog, Inc. + */ +package com.datadog.android.utils + +import com.datadog.android.internal.utils.appendIfNotEmpty +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import kotlin.math.pow +import kotlin.math.round +import kotlin.math.sqrt +import kotlin.system.measureNanoTime + +@Extensions( + ExtendWith(ForgeExtension::class) +) +internal class JointToStringVsStringBuilderPerformanceTest { + + @Test + fun `M be faster than joinToString W buildString`(forge: Forge) { + val itemsForJoin = forge.aList(ITEMS_TO_JOIN) { forge.aString() } + val joinToStringExecutionTime = mutableListOf() + val buildStringExecutionTime = mutableListOf() + + var jointToStringResult: String + var builderResult: String + + repeat(REPETITION_COUNT) { + joinToStringExecutionTime.add( + measureNanoTime { + val jointToStringContainer = mutableListOf() + for (item in itemsForJoin) { + jointToStringContainer.add(item) + } + jointToStringResult = jointToStringContainer.joinToString(separator = " ") { it } + } + ) + + buildStringExecutionTime.add( + measureNanoTime { + builderResult = buildString { + itemsForJoin.forEach { item -> appendIfNotEmpty(' ').append(item) } + } + } + ) + + assertThat(builderResult).isEqualTo(jointToStringResult) // same result + } + + val statisticsReport = ( + "buildString:\n" + + " mean = ${buildStringExecutionTime.mean}\n" + + " std = ${buildStringExecutionTime.std}\n" + + " cv = ${"%.2f".format(buildStringExecutionTime.cv)}%\n" + + " p50 = ${buildStringExecutionTime.percentile(50)}\n" + + " p90 = ${buildStringExecutionTime.percentile(90)}\n" + + " p95 = ${buildStringExecutionTime.percentile(95)}\n" + + " p99 = ${buildStringExecutionTime.percentile(99)}\n" + + "\n" + + "joinToString:\n" + + " mean = ${joinToStringExecutionTime.mean}\n" + + " std = ${joinToStringExecutionTime.std}\n" + + " cv = ${"%.2f".format(joinToStringExecutionTime.cv)}%\n" + + " p50 = ${joinToStringExecutionTime.percentile(50)},\n" + + " p90 = ${joinToStringExecutionTime.percentile(90)},\n" + + " p95 = ${joinToStringExecutionTime.percentile(95)},\n" + + " p99 = ${joinToStringExecutionTime.percentile(99)}\n" + ) + + assertThat( + buildStringExecutionTime.percentile(90) + ).withFailMessage( + statisticsReport + ).isLessThan( + joinToStringExecutionTime.percentile(90) + ) + } + + companion object { + private const val ITEMS_TO_JOIN = 10_000 + private const val REPETITION_COUNT = 10_000 + + private val List.mean + get() = (sum().toDouble() / size) + + private val List.std: Double + get() { + val m = mean + return sqrt( + sumOf { (it - m).pow(2.0) } / size + ) + } + + private val List.cv: Double + get() = std / mean * 100.0 + + private fun List.percentile(k: Int): Long { + val p = (k / 100.0) * (size + 1) + return sorted()[round(p).toInt()] + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/DeserializedMapAssert.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/DeserializedMapAssert.kt new file mode 100644 index 0000000000..a2ec94ba56 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/DeserializedMapAssert.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.assertj + +import com.datadog.android.utils.assertj.JsonElementAssert.Companion.assertThat +import com.google.gson.JsonElement +import org.assertj.core.api.AbstractAssert +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail +import java.util.Date + +internal class DeserializedMapAssert(actual: Map) : + AbstractAssert>( + actual, + DeserializedMapAssert::class.java + ) { + + fun isEqualTo(expectedMap: Map): DeserializedMapAssert { + assertThat(actual.size).overridingErrorMessage( + "We were expecting a map size: %d, actual it was: %d", + expectedMap.size, + actual.size + ).isEqualTo(expectedMap.size) + actual.forEach { + val expectedValue = expectedMap[it.key] + val currentValue = it.value + if (currentValue is JsonElement) { + assertThat(currentValue).isEqualTo(expectedValue) + } else if (currentValue is Number) { + when (expectedValue) { + is Long -> assertThat(currentValue.toLong()).isEqualTo(expectedValue) + is Double -> assertThat(currentValue.toDouble()).isEqualTo(expectedValue) + is Float -> assertThat(currentValue.toFloat()).isEqualTo(expectedValue) + is Byte -> assertThat(currentValue.toByte()).isEqualTo(expectedValue) + is Int -> assertThat(currentValue.toInt()).isEqualTo(expectedValue) + is Date -> assertThat(currentValue.toLong()).isEqualTo(expectedValue.time) + else -> fail("Unable to compare <$currentValue> with <$expectedValue>") + } + } else if (currentValue is String) { + assertThat(currentValue).isEqualTo(expectedValue.toString()) + } else { + assertThat(currentValue).isEqualTo(expectedValue) + } + } + + return this + } + + companion object { + fun assertThat(actual: Map): DeserializedMapAssert { + return DeserializedMapAssert(actual) + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/JsonElementAssert.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/JsonElementAssert.kt new file mode 100644 index 0000000000..a69c97b151 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/JsonElementAssert.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.assertj + +import com.datadog.android.core.internal.utils.toJsonArray +import com.datadog.android.core.internal.utils.toJsonObject +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.internal.LazilyParsedNumber +import org.assertj.core.api.AbstractAssert +import org.assertj.core.api.Assertions +import org.json.JSONArray +import org.json.JSONObject +import java.util.Date + +internal class JsonElementAssert(actual: JsonElement) : + AbstractAssert( + actual, + JsonElementAssert::class.java + ) { + + // region Assert + + override fun isEqualTo(expected: Any?): JsonElementAssert { + when (expected) { + null -> Assertions.assertThat(actual).isEqualTo(JsonNull.INSTANCE) + is Boolean -> Assertions.assertThat(actual.asBoolean).isEqualTo(expected) + is Int -> Assertions.assertThat(actual.asInt).isEqualTo(expected) + is Long -> Assertions.assertThat(actual.asLong).isEqualTo(expected) + is Float -> Assertions.assertThat(actual.asFloat).isEqualTo(expected) + is Double -> Assertions.assertThat(actual.asDouble).isEqualTo(expected) + is String -> Assertions.assertThat(actual.asString).isEqualTo(expected) + is Date -> Assertions.assertThat(actual.asLong).isEqualTo(expected.time) + is JsonNull -> Assertions.assertThat(actual).isEqualTo(JsonNull.INSTANCE) + is JsonObject -> Assertions.assertThat(actual.asJsonObject) + .usingRecursiveComparison() + .withComparatorForType(jsonPrimitivesComparator, JsonPrimitive::class.java) + .isEqualTo(expected) + is JsonArray -> Assertions.assertThat(actual.asJsonArray) + .usingRecursiveComparison() + .withComparatorForType(jsonPrimitivesComparator, JsonPrimitive::class.java) + .isEqualTo(expected) + is Iterable<*> -> Assertions.assertThat(actual.asJsonArray) + .usingRecursiveComparison() + .withComparatorForType(jsonPrimitivesComparator, JsonPrimitive::class.java) + .isEqualTo(expected.toJsonArray()) + is Map<*, *> -> Assertions.assertThat(actual.asJsonObject) + .usingRecursiveComparison() + .withComparatorForType(jsonPrimitivesComparator, JsonPrimitive::class.java) + .isEqualTo(expected.toJsonObject()) + is JSONArray -> Assertions.assertThat(actual.asJsonArray) + .usingRecursiveComparison() + .withComparatorForType(jsonPrimitivesComparator, JsonPrimitive::class.java) + .isEqualTo(expected.toJsonArray()) + is JSONObject -> Assertions.assertThat(actual.asJsonObject) + .usingRecursiveComparison() + .withComparatorForType(jsonPrimitivesComparator, JsonPrimitive::class.java) + .isEqualTo(expected.toJsonObject()) + else -> Assertions.assertThat(actual.asString).isEqualTo(expected.toString()) + } + return this + } + + // endregion + + // region Internal + + private val jsonPrimitivesComparator: (o1: JsonPrimitive, o2: JsonPrimitive) -> Int = + { o1, o2 -> + if (comparingFloatAndLazilyParsedNumber(o1, o2)) { + // when comparing a float with a LazilyParsedNumber the `JsonPrimitive#equals` + // method uses Double.parseValue(value) to convert the value from the + // LazilyParsedNumber and this method uses an extra precision. This will + // create assertion issues because even though the original values + // are the same the parsed values are no longer matching. + if (o1.asString.toDouble() == o2.asString.toDouble()) { + 0 + } else { + -1 + } + } else { + if (o1.equals(o2)) { + 0 + } else { + -1 + } + } + } + + private fun comparingFloatAndLazilyParsedNumber(o1: JsonPrimitive, o2: JsonPrimitive): Boolean { + return (o1.isNumber && o2.isNumber) && + (o1.asNumber is Float || o2.asNumber is Float) && + (o1.asNumber is LazilyParsedNumber || o2.asNumber is LazilyParsedNumber) + } + + // endregion + companion object { + fun assertThat(actual: JsonElement): JsonElementAssert { + return JsonElementAssert(actual) + } + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/NetworkInfoAssert.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/NetworkInfoAssert.kt similarity index 89% rename from dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/NetworkInfoAssert.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/NetworkInfoAssert.kt index b716aa407d..4c4c080757 100644 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/NetworkInfoAssert.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/NetworkInfoAssert.kt @@ -4,9 +4,9 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.log.assertj +package com.datadog.android.utils.assertj -import com.datadog.android.core.internal.net.info.NetworkInfo +import com.datadog.android.api.context.NetworkInfo import org.assertj.core.api.AbstractObjectAssert import org.assertj.core.api.Assertions.assertThat @@ -33,7 +33,7 @@ internal class NetworkInfoAssert(actual: NetworkInfo) : return this } - fun hasCarrierId(expected: Int): NetworkInfoAssert { + fun hasCarrierId(expected: Long?): NetworkInfoAssert { assertThat(actual.carrierId) .overridingErrorMessage( "Expected networkInfo to have carrierId $expected " + @@ -53,7 +53,7 @@ internal class NetworkInfoAssert(actual: NetworkInfo) : return this } - fun hasUpSpeed(expected: Int): NetworkInfoAssert { + fun hasUpSpeed(expected: Long?): NetworkInfoAssert { assertThat(actual.upKbps) .overridingErrorMessage( "Expected networkInfo to have upKbps $expected " + @@ -63,7 +63,7 @@ internal class NetworkInfoAssert(actual: NetworkInfo) : return this } - fun hasDownSpeed(expected: Int): NetworkInfoAssert { + fun hasDownSpeed(expected: Long?): NetworkInfoAssert { assertThat(actual.downKbps) .overridingErrorMessage( "Expected networkInfo to have downKbps $expected " + @@ -73,7 +73,7 @@ internal class NetworkInfoAssert(actual: NetworkInfo) : return this } - fun hasStrength(expected: Int): NetworkInfoAssert { + fun hasStrength(expected: Long?): NetworkInfoAssert { assertThat(actual.strength) .overridingErrorMessage( "Expected networkInfo to have strength $expected " + diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/SystemInfoAssert.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/SystemInfoAssert.kt new file mode 100644 index 0000000000..610c971a92 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/assertj/SystemInfoAssert.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.assertj + +import com.datadog.android.core.internal.system.SystemInfo +import org.assertj.core.api.AbstractObjectAssert +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import kotlin.math.max + +internal class SystemInfoAssert(actual: SystemInfo) : + AbstractObjectAssert(actual, SystemInfoAssert::class.java) { + + fun hasPowerSaveMode(expected: Boolean): SystemInfoAssert { + assertThat(actual.powerSaveMode) + .overridingErrorMessage( + "Expected systemInfo to have powerSaveMode $expected " + + "but was ${actual.powerSaveMode}" + ) + .isEqualTo(expected) + return this + } + + fun hasBatteryLevel(expected: Int, scale: Int = 100): SystemInfoAssert { + assertThat(actual.batteryLevel) + .overridingErrorMessage( + "Expected systemInfo to have batteryLevel $expected " + + "but was ${actual.batteryLevel}" + ) + .isCloseTo(expected, Offset.offset(max(100 / scale, 1))) + + return this + } + + fun hasBatteryFullOrCharging(expected: Boolean): SystemInfoAssert { + assertThat(actual.batteryFullOrCharging).overridingErrorMessage( + "Expected systemInfo to have batteryFullOrCharging flag $expected " + + "but was ${actual.batteryFullOrCharging}" + ).isEqualTo(expected) + return this + } + + fun hasOnExternalPowerSource(expected: Boolean): SystemInfoAssert { + assertThat(actual.onExternalPowerSource).overridingErrorMessage( + "Expected systemInfo to have onExternalPowerSource flag $expected " + + "but was ${actual.onExternalPowerSource}" + ).isEqualTo(expected) + return this + } + + companion object { + + internal fun assertThat(actual: SystemInfo): SystemInfoAssert = + SystemInfoAssert(actual) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/ApplicationContextTestConfiguration.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/ApplicationContextTestConfiguration.kt new file mode 100644 index 0000000000..2b69af3e20 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/ApplicationContextTestConfiguration.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.config + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.AssetManager +import com.datadog.android.core.internal.CoreFeature +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import fr.xgouchet.elmyr.Forge +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.io.File +import java.nio.file.Files +import java.util.UUID + +internal open class ApplicationContextTestConfiguration(klass: Class) : + MockTestConfiguration(klass) { + + lateinit var fakePackageName: String + lateinit var fakeVersionName: String + var fakeVersionCode: Int = 0 + lateinit var fakeVariant: String + lateinit var fakeBuildId: String + + lateinit var fakePackageInfo: PackageInfo + lateinit var fakeAppInfo: ApplicationInfo + lateinit var mockPackageManager: PackageManager + lateinit var mockAssetManager: AssetManager + + lateinit var fakeSandboxDir: File + lateinit var fakeCacheDir: File + lateinit var fakeFilesDir: File + + // region ApplicationContextTestConfiguration + + override fun setUp(forge: Forge) { + super.setUp(forge) + + createFakeInfo(forge) + mockPackageManager() + mockAssetManager() + + whenever(mockInstance.applicationContext) doReturn mockInstance + whenever(mockInstance.packageManager) doReturn mockPackageManager + whenever(mockInstance.packageName) doReturn fakePackageName + whenever(mockInstance.applicationInfo) doReturn fakeAppInfo + whenever(mockInstance.assets) doReturn mockAssetManager + + // ??? + whenever(mockInstance.getSystemService(Context.ACTIVITY_SERVICE)) doReturn mock() + whenever(mockInstance.getSharedPreferences(any(), any())) doReturn mock() + + // Filesystem + fakeSandboxDir = Files.createTempDirectory("app-context").toFile() + fakeCacheDir = File(fakeSandboxDir, "cache") + fakeFilesDir = File(fakeSandboxDir, "files") + whenever(mockInstance.cacheDir) doReturn fakeCacheDir + whenever(mockInstance.filesDir) doReturn fakeFilesDir + } + + override fun tearDown(forge: Forge) { + super.tearDown(forge) + fakeSandboxDir.deleteRecursively() + } + + // endregion + + // region Internal + + private fun createFakeInfo(forge: Forge) { + fakePackageName = forge.aStringMatching("[a-z]{2,4}(\\.[a-z]{3,8}){2,4}") + fakeVersionName = forge.aStringMatching("[0-9](\\.[0-9]{1,3}){2,3}") + fakeVersionCode = forge.anInt(1, 65536) + fakeVariant = forge.anElementFrom(forge.anAlphabeticalString(), "") + fakeBuildId = forge.getForgery().toString() + + fakePackageInfo = PackageInfo() + fakePackageInfo.packageName = fakePackageName + fakePackageInfo.versionName = fakeVersionName + @Suppress("DEPRECATION") + fakePackageInfo.versionCode = fakeVersionCode + fakePackageInfo.longVersionCode = fakeVersionCode.toLong() + + fakeAppInfo = ApplicationInfo() + } + + private fun mockPackageManager() { + mockPackageManager = mock() + whenever( + mockPackageManager.getPackageInfo( + fakePackageName, + PackageManager.PackageInfoFlags.of(0) + ) + ) doReturn fakePackageInfo + whenever(mockPackageManager.getPackageInfo(fakePackageName, 0)) doReturn fakePackageInfo + } + + private fun mockAssetManager() { + mockAssetManager = mock() + whenever( + mockAssetManager.open(CoreFeature.BUILD_ID_FILE_NAME) + ) doReturn fakeBuildId.byteInputStream() + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/CoreFeatureTestConfiguration.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/CoreFeatureTestConfiguration.kt new file mode 100644 index 0000000000..b3d2e649f5 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/CoreFeatureTestConfiguration.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.config + +import android.content.Context +import com.datadog.android.DatadogSite +import com.datadog.android.core.configuration.BatchSize +import com.datadog.android.core.configuration.UploadFrequency +import com.datadog.android.core.internal.CoreFeature +import com.datadog.android.core.internal.account.MutableAccountInfoProvider +import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver +import com.datadog.android.core.internal.net.info.NetworkInfoProvider +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.internal.system.AndroidInfoProvider +import com.datadog.android.core.internal.system.AppVersionProvider +import com.datadog.android.core.internal.system.SystemInfoProvider +import com.datadog.android.core.internal.user.MutableUserInfoProvider +import com.datadog.android.core.thread.FlushableExecutorService +import com.datadog.android.internal.time.TimeProvider +import com.datadog.android.privacy.TrackingConsent +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import com.lyft.kronos.KronosClock +import fr.xgouchet.elmyr.Forge +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.io.File +import java.lang.ref.WeakReference +import java.nio.file.Files +import java.util.Locale +import java.util.UUID +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.ThreadPoolExecutor + +internal class CoreFeatureTestConfiguration( + val appContext: ApplicationContextTestConfiguration +) : MockTestConfiguration(CoreFeature::class.java) { + + lateinit var fakeServiceName: String + lateinit var fakeEnvName: String + lateinit var fakeSourceName: String + lateinit var fakeClientToken: String + lateinit var fakeSdkVersion: String + lateinit var fakeStorageDir: File + lateinit var fakeUploadFrequency: UploadFrequency + lateinit var fakeSite: DatadogSite + lateinit var fakeFilePersistenceConfig: FilePersistenceConfig + lateinit var fakeBatchSize: BatchSize + var fakeBuildId: String? = null + + lateinit var callFactory: CoreFeature.OkHttpCallFactory + + lateinit var mockUploadExecutor: ScheduledThreadPoolExecutor + lateinit var mockPersistenceExecutor: FlushableExecutorService + lateinit var mockContextExecutorService: ThreadPoolExecutor + lateinit var mockKronosClock: KronosClock + lateinit var mockContextRef: WeakReference + lateinit var mockFirstPartyHostHeaderTypeResolver: DefaultFirstPartyHostHeaderTypeResolver + + lateinit var mockTimeProvider: TimeProvider + lateinit var mockNetworkInfoProvider: NetworkInfoProvider + lateinit var mockSystemInfoProvider: SystemInfoProvider + lateinit var mockUserInfoProvider: MutableUserInfoProvider + lateinit var mockAccountInfoProvider: MutableAccountInfoProvider + lateinit var mockTrackingConsentProvider: ConsentProvider + lateinit var mockAndroidInfoProvider: AndroidInfoProvider + lateinit var mockAppVersionProvider: AppVersionProvider + + // region CoreFeatureTestConfiguration + + override fun setUp(forge: Forge) { + super.setUp(forge) + createFakeInfo(forge) + createMocks() + configureCoreFeature() + } + + override fun tearDown(forge: Forge) { + fakeStorageDir.deleteRecursively() + } + + // endregion + + // region Internal + + private fun createFakeInfo(forge: Forge) { + fakeEnvName = forge.anAlphabeticalString() + fakeServiceName = forge.anAlphabeticalString() + fakeSourceName = forge.anAlphabeticalString() + fakeClientToken = forge.anHexadecimalString().lowercase(Locale.US) + fakeSdkVersion = forge.aStringMatching("[0-9](\\.[0-9]{1,2}){1,3}") + fakeStorageDir = Files.createTempDirectory(forge.anHexadecimalString()).toFile() + fakeUploadFrequency = forge.aValueFrom(UploadFrequency::class.java) + fakeSite = forge.aValueFrom(DatadogSite::class.java) + fakeFilePersistenceConfig = forge.getForgery() + fakeBatchSize = forge.aValueFrom(BatchSize::class.java) + fakeBuildId = forge.aNullable { getForgery().toString() } + } + + private fun createMocks() { + mockPersistenceExecutor = mock() + mockContextExecutorService = mock() + mockUploadExecutor = mock() + callFactory = CoreFeature.OkHttpCallFactory { + mock() + } + + mockKronosClock = mock() + // Mockito cannot mock WeakReference by some reason + mockContextRef = WeakReference(appContext.mockInstance) + mockFirstPartyHostHeaderTypeResolver = mock() + + mockTimeProvider = mock() + mockNetworkInfoProvider = mock() + mockSystemInfoProvider = mock() + mockUserInfoProvider = mock() + mockAccountInfoProvider = mock() + mockAndroidInfoProvider = mock() + mockTrackingConsentProvider = mock { on { getConsent() } doReturn TrackingConsent.PENDING } + mockAppVersionProvider = mock { on { version } doReturn appContext.fakeVersionName } + } + + private fun configureCoreFeature() { + whenever(mockInstance.isMainProcess) doReturn true + whenever(mockInstance.envName) doReturn fakeEnvName + whenever(mockInstance.serviceName) doReturn fakeServiceName + whenever(mockInstance.packageVersionProvider) doReturn mockAppVersionProvider + whenever(mockInstance.variant) doReturn appContext.fakeVariant + whenever(mockInstance.sourceName) doReturn fakeSourceName + whenever(mockInstance.clientToken) doReturn fakeClientToken + whenever(mockInstance.sdkVersion) doReturn fakeSdkVersion + whenever(mockInstance.storageDir) doReturn fakeStorageDir + whenever(mockInstance.uploadFrequency) doReturn fakeUploadFrequency + whenever(mockInstance.site) doReturn fakeSite + whenever(mockInstance.appBuildId) doReturn fakeBuildId + + whenever(mockInstance.persistenceExecutorService) doReturn mockPersistenceExecutor + whenever(mockInstance.contextExecutorService) doReturn mockContextExecutorService + whenever(mockInstance.uploadExecutorService) doReturn mockUploadExecutor + whenever(mockInstance.callFactory) doReturn callFactory + whenever(mockInstance.kronosClock) doReturn mockKronosClock + whenever(mockInstance.contextRef) doReturn mockContextRef + whenever(mockInstance.firstPartyHostHeaderTypeResolver) doReturn mockFirstPartyHostHeaderTypeResolver + + whenever(mockInstance.timeProvider) doReturn mockTimeProvider + whenever(mockInstance.networkInfoProvider) doReturn mockNetworkInfoProvider + whenever(mockInstance.systemInfoProvider) doReturn mockSystemInfoProvider + whenever(mockInstance.userInfoProvider) doReturn mockUserInfoProvider + whenever(mockInstance.accountInfoProvider) doReturn mockAccountInfoProvider + whenever(mockInstance.trackingConsentProvider) doReturn mockTrackingConsentProvider + whenever(mockInstance.androidInfoProvider) doReturn mockAndroidInfoProvider + + whenever(mockInstance.buildFilePersistenceConfig()) doReturn fakeFilePersistenceConfig.copy( + recentDelayMs = fakeBatchSize.windowDurationMs + ) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/InternalLoggerTestConfiguration.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/InternalLoggerTestConfiguration.kt new file mode 100644 index 0000000000..67f1456a45 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/config/InternalLoggerTestConfiguration.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.config + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.utils.unboundInternalLogger +import com.datadog.tools.unit.extensions.config.TestConfiguration +import fr.xgouchet.elmyr.Forge +import org.mockito.kotlin.mock + +internal class InternalLoggerTestConfiguration : TestConfiguration { + + lateinit var mockInternalLogger: InternalLogger + + private lateinit var originalInternalLogger: InternalLogger + + override fun setUp(forge: Forge) { + mockInternalLogger = mock() + + originalInternalLogger = unboundInternalLogger + + unboundInternalLogger = mockInternalLogger + } + + override fun tearDown(forge: Forge) { + unboundInternalLogger = originalInternalLogger + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/utils/extension/JsonObjectExt.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/extension/JsonObjectExt.kt similarity index 100% rename from dd-sdk-android/src/test/kotlin/com/datadog/android/utils/extension/JsonObjectExt.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/extension/JsonObjectExt.kt diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/AndroidInfoProviderForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/AndroidInfoProviderForgeryFactory.kt new file mode 100644 index 0000000000..7125af2d21 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/AndroidInfoProviderForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.api.context.DeviceType +import com.datadog.android.core.internal.system.AndroidInfoProvider +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class AndroidInfoProviderForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): AndroidInfoProvider { + val deviceName = forge.aString() + val deviceBrand = forge.aString() + val deviceModel = forge.aString() + val deviceType = forge.aValueFrom(DeviceType::class.java) + val deviceBuildId = forge.aString() + val osName = forge.aString() + val osMajorVersion = forge.aSmallInt().toString() + val osVersion = "${forge.aSmallInt()}.${forge.aSmallInt()}.${forge.aSmallInt()}" + val architecture = forge.anAlphaNumericalString() + val numberOfDisplays = forge.aNullable { forge.anInt() } + val locales = forge.aList { forge.aString() } + val currentLocale = forge.aString() + val timeZone = forge.aString() + + return object : AndroidInfoProvider { + override val deviceName = deviceName + override val deviceBrand = deviceBrand + override val deviceModel = deviceModel + override val deviceType = deviceType + override val deviceBuildId = deviceBuildId + override val osName = osName + override val osMajorVersion = osMajorVersion + override val osVersion = osVersion + override val architecture = architecture + override val numberOfDisplays = numberOfDisplays + override val locales: List = locales + override val currentLocale: String = currentLocale + override val timeZone: String = timeZone + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchClosedMetadataForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchClosedMetadataForgeryFactory.kt new file mode 100644 index 0000000000..10803fc6ff --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchClosedMetadataForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.metrics.BatchClosedMetadata +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class BatchClosedMetadataForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): BatchClosedMetadata { + return BatchClosedMetadata( + lastTimeWasUsedInMs = forge.aPositiveLong(), + eventsCount = forge.aPositiveLong() + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchDataForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchDataForgeryFactory.kt new file mode 100644 index 0000000000..6380528727 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchDataForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.persistence.BatchData +import com.datadog.android.core.internal.persistence.BatchId +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class BatchDataForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): BatchData { + return BatchData( + id = BatchId(id = forge.anAlphaNumericalString()), + data = forge.aList { getForgery() }, + metadata = forge.aNullable { + forge.aString( + forge.anInt(min = 0, max = DATA_SIZE_LIMIT) + ).toByteArray() + } + ) + } + companion object { + const val DATA_SIZE_LIMIT = 32 + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/utils/forge/BatchForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchForgeryFactory.kt similarity index 91% rename from dd-sdk-android/src/test/kotlin/com/datadog/android/utils/forge/BatchForgeryFactory.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchForgeryFactory.kt index 3210a689ae..9bde66ea6f 100644 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/utils/forge/BatchForgeryFactory.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchForgeryFactory.kt @@ -6,7 +6,7 @@ package com.datadog.android.utils.forge -import com.datadog.android.core.internal.data.file.Batch +import com.datadog.android.core.internal.persistence.Batch import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchIdForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchIdForgeryFactory.kt new file mode 100644 index 0000000000..f8ad80d1b1 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BatchIdForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.persistence.BatchId +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class BatchIdForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): BatchId { + return BatchId(id = forge.anAlphaNumericalString()) + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/utils/forge/BigIntegerFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BigIntegerFactory.kt similarity index 100% rename from dd-sdk-android/src/test/kotlin/com/datadog/android/utils/forge/BigIntegerFactory.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/BigIntegerFactory.kt diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CharsetForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CharsetForgeryFactory.kt new file mode 100644 index 0000000000..d29590c5ac --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CharsetForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import java.nio.charset.Charset + +class CharsetForgeryFactory : ForgeryFactory { + /** + * @param forge the forge instance to use to generate a forgery + * @return a new instance of type T, randomly generated with the help of the forge instance + */ + override fun getForgery(forge: Forge): Charset { + return forge.anElementFrom( + Charsets.ISO_8859_1, + Charsets.US_ASCII, + Charsets.UTF_8, + Charsets.UTF_16, + Charsets.UTF_32 + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationCoreForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationCoreForgeryFactory.kt new file mode 100644 index 0000000000..54708b6bef --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationCoreForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.DatadogSite +import com.datadog.android.core.configuration.BackPressureMitigation +import com.datadog.android.core.configuration.BackPressureStrategy +import com.datadog.android.core.configuration.Configuration +import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.security.NoOpEncryption +import com.datadog.android.trace.TracingHeaderType +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import okhttp3.Authenticator +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.net.Proxy +import java.net.URL + +internal class ConfigurationCoreForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): Configuration.Core { + val (proxy, auth) = if (forge.aBool()) { + mock() to mock() + } else { + null to Authenticator.NONE + } + + return Configuration.Core( + needsClearTextHttp = forge.aBool(), + enableDeveloperModeWhenDebuggable = forge.aBool(), + firstPartyHostsWithHeaderTypes = forge.aMap { + getForgery().host to aList { + aValueFrom( + TracingHeaderType::class.java + ) + }.toSet() + }, + batchSize = forge.getForgery(), + uploadFrequency = forge.getForgery(), + proxy = proxy, + proxyAuth = auth, + encryption = forge.aNullable { NoOpEncryption() }, + site = forge.aValueFrom(DatadogSite::class.java), + batchProcessingLevel = forge.getForgery(), + persistenceStrategyFactory = forge.aNullable { + mock().apply { + whenever(create(any(), any(), any())) doReturn mock() + } + }, + backpressureStrategy = BackPressureStrategy( + forge.aSmallInt(), + mock(), + mock(), + forge.aValueFrom(BackPressureMitigation::class.java) + ), + uploadSchedulerStrategy = forge.aNullable { mock() } + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationForgeryFactory.kt new file mode 100644 index 0000000000..3a011351f0 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.configuration.Configuration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class ConfigurationForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): Configuration { + return Configuration( + coreConfig = forge.getForgery(), + clientToken = forge.anHexadecimalString(), + env = forge.aStringMatching("[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]"), + variant = forge.anElementFrom(forge.anAlphabeticalString(), ""), + service = forge.aStringMatching("[a-z]+(\\.[a-z]+)+"), + crashReportsEnabled = forge.aBool(), + additionalConfig = forge.aMap { aString() to aString() } + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/Configurator.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/Configurator.kt new file mode 100644 index 0000000000..e0c170e054 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/Configurator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.internal.tests.elmyr.InternalTelemetryApiUsageForgeryFactory +import com.datadog.android.internal.tests.elmyr.TracingHeaderTypesSetForgeryFactory +import com.datadog.android.test.elmyr.PersistenceStrategyBatchForgeryFactory +import com.datadog.android.tests.elmyr.useCoreFactories +import com.datadog.tools.unit.forge.BaseConfigurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.jvm.useJvmFactories + +internal class Configurator : + BaseConfigurator() { + override fun configure(forge: Forge) { + super.configure(forge) + forge.useCoreFactories() + + // Datadog Core + forge.addFactory(CustomAttributesForgeryFactory()) + forge.addFactory(ConfigurationForgeryFactory()) + forge.addFactory(ConfigurationCoreForgeryFactory()) + forge.addFactory(FilePersistenceConfigForgeryFactory()) + forge.addFactory(AndroidInfoProviderForgeryFactory()) + forge.addFactory(FeatureStorageConfigurationForgeryFactory()) + forge.addFactory(BatchDataForgeryFactory()) + forge.addFactory(BatchIdForgeryFactory()) + + // IO + forge.addFactory(BatchForgeryFactory()) + forge.addFactory(PayloadDecorationForgeryFactory()) + forge.addFactory(WorkerParametersForgeryFactory()) + + // NDK Crash + forge.addFactory(NdkCrashLogForgeryFactory()) + + // MISC + forge.addFactory(BigIntegerFactory()) + forge.addFactory(CharsetForgeryFactory()) + + // Datadog SDK v2 + forge.addFactory(DataUploadConfigurationForgeryFactory()) + + // UploadStatus + forge.addFactory(DNSErrorStatusForgeryFactory()) + forge.addFactory(HttpClientErrorForgeryFactory()) + forge.addFactory(HttpClientRateLimitingStatusForgeryFactory()) + forge.addFactory(HttpRedirectStatusForgeryFactory()) + forge.addFactory(HttpServerErrorForgeryFactory()) + forge.addFactory(InvalidTokenErrorStatusForgeryFactory()) + forge.addFactory(NetworkErrorStatusForgeryFactory()) + forge.addFactory(RequestCreationErrorStatusForgeryFactory()) + forge.addFactory(SuccessStatusForgeryFactory()) + forge.addFactory(UnknownExceptionStatusForgeryFactory()) + forge.addFactory(UnknownHttpErrorStatusForgeryFactory()) + forge.addFactory(UnknownStatusForgeryFactory()) + + // RemovalReason + forge.addFactory(RemovalReasonFlushedForgeryFactory()) + forge.addFactory(RemovalReasonPurgedForgeryFactory()) + forge.addFactory(RemovalReasonInvalidForgeryFactory()) + forge.addFactory(RemovalReasonObsoleteForgeryFactory()) + forge.addFactory(RemovalReasonIntakeCodeForgeryFactory()) + forge.addFactory(RemovalReasonForgeryFactory()) + + forge.addFactory(BatchClosedMetadataForgeryFactory()) + + forge.addFactory(PersistenceStrategyBatchForgeryFactory()) + + forge.useJvmFactories() + + // telemetry + forge.addFactory(InternalTelemetryApiUsageForgeryFactory()) + forge.addFactory(TracingHeaderTypesSetForgeryFactory()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CustomAttributes.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CustomAttributes.kt new file mode 100644 index 0000000000..629e585e65 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CustomAttributes.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +data class CustomAttributes( + val nullableData: Map, + val nonNullData: Map +) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CustomAttributesForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CustomAttributesForgeryFactory.kt new file mode 100644 index 0000000000..6ec6e95302 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/CustomAttributesForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +internal class CustomAttributesForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): CustomAttributes { + return CustomAttributes( + nullableData = forge.run { + listOf( + aBool(), + anInt(), + aLong(), + aFloat(), + aDouble(), + anAsciiString(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + aList { anAlphabeticalString() }, + aList { aDouble() }, + null + ) + .map { anAlphaNumericalString() to it } + .toMap() + }, + nonNullData = forge.run { + listOf( + aBool(), + anInt(), + aLong(), + aFloat(), + aDouble(), + anAsciiString(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + aList { anAlphabeticalString() }, + aList { aDouble() } + ) + .map { anAlphaNumericalString() to it } + .toMap() + } + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/DNSErrorStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/DNSErrorStatusForgeryFactory.kt new file mode 100644 index 0000000000..9d5e00c6da --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/DNSErrorStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import com.datadog.tools.unit.forge.anException +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class DNSErrorStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.DNSError { + return UploadStatus.DNSError(forge.anException()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/DataUploadConfigurationForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/DataUploadConfigurationForgeryFactory.kt new file mode 100644 index 0000000000..f71fb2bb04 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/DataUploadConfigurationForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.configuration.UploadFrequency +import com.datadog.android.core.internal.configuration.DataUploadConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class DataUploadConfigurationForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): DataUploadConfiguration { + val frequency: UploadFrequency = forge.getForgery() + // we limit the size to avoid OOM errors inside our tests + return DataUploadConfiguration(frequency, forge.anInt(min = 1, max = 200)) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/FeatureStorageConfigurationForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/FeatureStorageConfigurationForgeryFactory.kt new file mode 100644 index 0000000000..abc51bdb4e --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/FeatureStorageConfigurationForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.api.storage.FeatureStorageConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class FeatureStorageConfigurationForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): FeatureStorageConfiguration { + return FeatureStorageConfiguration( + maxBatchSize = forge.aPositiveLong(), + maxItemsPerBatch = forge.aBigInt(), + maxItemSize = forge.aPositiveLong(), + oldBatchThreshold = forge.aPositiveLong() + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/FilePersistenceConfigForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/FilePersistenceConfigForgeryFactory.kt new file mode 100644 index 0000000000..068e65c926 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/FilePersistenceConfigForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class FilePersistenceConfigForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): FilePersistenceConfig { + return FilePersistenceConfig( + recentDelayMs = forge.aPositiveLong(), + maxBatchSize = forge.aPositiveLong(), + maxItemsPerBatch = forge.aBigInt(), + oldFileThreshold = forge.aPositiveLong(), + maxDiskSpace = forge.aPositiveLong() + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpClientErrorForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpClientErrorForgeryFactory.kt new file mode 100644 index 0000000000..011121d830 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpClientErrorForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class HttpClientErrorForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.HttpClientError { + return UploadStatus.HttpClientError(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpClientRateLimitingStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpClientRateLimitingStatusForgeryFactory.kt new file mode 100644 index 0000000000..4d4679511f --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpClientRateLimitingStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class HttpClientRateLimitingStatusForgeryFactory : + ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.HttpClientRateLimiting { + return UploadStatus.HttpClientRateLimiting(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpRedirectStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpRedirectStatusForgeryFactory.kt new file mode 100644 index 0000000000..476175e9b2 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpRedirectStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class HttpRedirectStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.HttpRedirection { + return UploadStatus.HttpRedirection(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpServerErrorForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpServerErrorForgeryFactory.kt new file mode 100644 index 0000000000..06c9c5eb54 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/HttpServerErrorForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class HttpServerErrorForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.HttpServerError { + return UploadStatus.HttpServerError(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/InvalidTokenErrorStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/InvalidTokenErrorStatusForgeryFactory.kt new file mode 100644 index 0000000000..11dcbebb0c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/InvalidTokenErrorStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class InvalidTokenErrorStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.InvalidTokenError { + return UploadStatus.InvalidTokenError(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/NdkCrashLogForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/NdkCrashLogForgeryFactory.kt new file mode 100644 index 0000000000..2148b51fb4 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/NdkCrashLogForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.ndk.internal.NdkCrashLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class NdkCrashLogForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): NdkCrashLog { + return NdkCrashLog( + signal = forge.anInt(min = 1), + timestamp = System.currentTimeMillis(), + timeSinceAppStartMs = forge.aNullable { aPositiveLong() }, + signalName = forge.anAlphabeticalString(), + message = forge.anAlphabeticalString(), + stacktrace = forge.anAlphabeticalString() + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/NetworkErrorStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/NetworkErrorStatusForgeryFactory.kt new file mode 100644 index 0000000000..91803d4ab9 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/NetworkErrorStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import com.datadog.tools.unit.forge.anException +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class NetworkErrorStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.NetworkError { + return UploadStatus.NetworkError(forge.anException()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/PayloadDecorationForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/PayloadDecorationForgeryFactory.kt new file mode 100644 index 0000000000..edb7f40a61 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/PayloadDecorationForgeryFactory.kt @@ -0,0 +1,35 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.persistence.PayloadDecoration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class PayloadDecorationForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): PayloadDecoration { + val pair = forge.anElementFrom( + "(" to ")", + "{" to "}", + "[" to "]", + "[[" to "]]", + "<" to ">", + "", + "\"" to "\"", + "'" to "'", + "“" to "”", + "‘" to "’" + ) + return forge.anElementFrom( + PayloadDecoration( + pair.first, + pair.second, + forge.anElementFrom("|", ",", ";", ":", "&", "/", ".", "-") + ) + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonFlushedForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonFlushedForgeryFactory.kt new file mode 100644 index 0000000000..601435992a --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonFlushedForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.metrics.RemovalReason +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class RemovalReasonFlushedForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): RemovalReason.Flushed { + return RemovalReason.Flushed + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonForgeryFactory.kt new file mode 100644 index 0000000000..93d0069d33 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.metrics.RemovalReason +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class RemovalReasonForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): RemovalReason { + return forge.anElementFrom( + listOf( + forge.getForgery(RemovalReason.Purged::class.java), + forge.getForgery(RemovalReason.Obsolete::class.java), + forge.getForgery(RemovalReason.Flushed::class.java), + forge.getForgery(RemovalReason.Invalid::class.java), + forge.getForgery(RemovalReason.IntakeCode::class.java) + ) + ) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonIntakeCodeForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonIntakeCodeForgeryFactory.kt new file mode 100644 index 0000000000..254baf8309 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonIntakeCodeForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.metrics.RemovalReason +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class RemovalReasonIntakeCodeForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): RemovalReason.IntakeCode { + return RemovalReason.IntakeCode(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonInvalidForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonInvalidForgeryFactory.kt new file mode 100644 index 0000000000..fd82f2e72f --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonInvalidForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.metrics.RemovalReason +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class RemovalReasonInvalidForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): RemovalReason.Invalid { + return RemovalReason.Invalid + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonObsoleteForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonObsoleteForgeryFactory.kt new file mode 100644 index 0000000000..7c5592fba7 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonObsoleteForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.metrics.RemovalReason +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class RemovalReasonObsoleteForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): RemovalReason.Obsolete { + return RemovalReason.Obsolete + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonPurgedForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonPurgedForgeryFactory.kt new file mode 100644 index 0000000000..9b67e94ee6 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RemovalReasonPurgedForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.metrics.RemovalReason +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class RemovalReasonPurgedForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): RemovalReason.Purged { + return RemovalReason.Purged + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RequestCreationErrorStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RequestCreationErrorStatusForgeryFactory.kt new file mode 100644 index 0000000000..759a58777b --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/RequestCreationErrorStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import com.datadog.tools.unit.forge.anException +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class RequestCreationErrorStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.RequestCreationError { + return UploadStatus.RequestCreationError(forge.anException()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/SuccessStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/SuccessStatusForgeryFactory.kt new file mode 100644 index 0000000000..e63d26af1b --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/SuccessStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class SuccessStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.Success { + return UploadStatus.Success(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownExceptionStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownExceptionStatusForgeryFactory.kt new file mode 100644 index 0000000000..1845923449 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownExceptionStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import com.datadog.tools.unit.forge.anException +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class UnknownExceptionStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.UnknownException { + return UploadStatus.UnknownException(forge.anException()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownHttpErrorStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownHttpErrorStatusForgeryFactory.kt new file mode 100644 index 0000000000..53023ca343 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownHttpErrorStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class UnknownHttpErrorStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.UnknownHttpError { + return UploadStatus.UnknownHttpError(responseCode = forge.aPositiveInt()) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownStatusForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownStatusForgeryFactory.kt new file mode 100644 index 0000000000..cfd5732b0e --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/UnknownStatusForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.core.internal.data.upload.UploadStatus +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class UnknownStatusForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UploadStatus.UnknownStatus { + return UploadStatus.UnknownStatus + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/WorkerParametersForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/WorkerParametersForgeryFactory.kt new file mode 100644 index 0000000000..d2eb401d1c --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/WorkerParametersForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import android.content.Context +import androidx.work.Data +import androidx.work.ListenableWorker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import androidx.work.impl.utils.taskexecutor.SerialExecutor +import androidx.work.impl.utils.taskexecutor.TaskExecutor +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +class WorkerParametersForgeryFactory : ForgeryFactory { + + // region ForgeryFactory + + override fun getForgery(forge: Forge): WorkerParameters { + val threadExecutor = Executors.newSingleThreadExecutor() + return WorkerParameters( + forge.getForgery(), + Data.EMPTY, + forge.aList { anAlphabeticalString() }, + WorkerParameters.RuntimeExtras(), + forge.aSmallInt(), + forge.aSmallInt(), + threadExecutor, + object : TaskExecutor { + override fun getMainThreadExecutor(): Executor { + TODO() + } + + override fun getSerialTaskExecutor(): SerialExecutor { + TODO("Not yet implemented") + } + }, + object : WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + return null + } + }, + { _, _, _ -> forge.getForgery() }, + { _, _, _ -> forge.getForgery() } + ) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/test/resources/logs-batch-2.2.0-and-earlier b/dd-sdk-android-core/src/test/resources/logs-batch-2.2.0-and-earlier new file mode 100644 index 0000000000..0e09e29cac Binary files /dev/null and b/dd-sdk-android-core/src/test/resources/logs-batch-2.2.0-and-earlier differ diff --git a/dd-sdk-android-coil/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/dd-sdk-android-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from dd-sdk-android-coil/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to dd-sdk-android-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/dd-sdk-android-core/src/testDebug/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerDebugTest.kt b/dd-sdk-android-core/src/testDebug/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerDebugTest.kt new file mode 100644 index 0000000000..4e51ea205d --- /dev/null +++ b/dd-sdk-android-core/src/testDebug/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerDebugTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.logger + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.kotlin.mock + +@Extensions( + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +internal class SdkInternalLoggerDebugTest { + + // region sdkLogger + + @Test + @Suppress("FunctionNaming") + fun `M build LogCat sdkLogger W init()`( + forge: Forge + ) { + // When + val logger = SdkInternalLogger(forge.aNullable { mock() }) + + // Then + val handler: LogcatLogHandler? = logger.maintainerLogger + assertThat(handler).isNotNull + assertThat(handler?.tag).isEqualTo(SdkInternalLogger.SDK_LOG_TAG) + } + + // endregion +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/api/feature/SdkFeatureMock.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/api/feature/SdkFeatureMock.kt new file mode 100644 index 0000000000..72ab36afb6 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/api/feature/SdkFeatureMock.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.feature + +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.core.internal.SdkFeature +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.util.concurrent.Future + +object SdkFeatureMock { + /** + * This method is a trick that allows to mock FeatureScope.getContextFuture extension method. + */ + fun create(future: Future? = null): FeatureScope = mock { + on { getContextFuture(any()) } doReturn future + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt new file mode 100644 index 0000000000..da0c04c86b --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.context.AccountInfo +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class AccountInfoForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): AccountInfo { + return AccountInfo( + id = forge.anHexadecimalString(), + name = forge.aNullable { forge.aStringMatching("[A-Z][a-z]+ [A-Z]\\. [A-Z][a-z]+") }, + extraInfo = forge.exhaustiveAttributes( + excludedKeys = setOf( + "id", + "name" + ) + ) + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt new file mode 100644 index 0000000000..357fd04c17 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt @@ -0,0 +1,44 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.DatadogSite +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.privacy.TrackingConsent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import java.util.Locale +import java.util.UUID + +class DatadogContextForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): DatadogContext { + return DatadogContext( + site = forge.aValueFrom(DatadogSite::class.java), + clientToken = forge.anHexadecimalString().lowercase(Locale.US), + service = forge.anAlphabeticalString(), + version = forge.aStringMatching("[0-9](\\.[0-9]{1,3}){2,3}"), + variant = forge.anAlphabeticalString(), + env = forge.anAlphabeticalString().lowercase(Locale.US), + source = forge.anAlphabeticalString(), + sdkVersion = forge.aStringMatching("[0-9](\\.[0-9]{1,2}){1,3}"), + time = forge.getForgery(), + processInfo = forge.getForgery(), + networkInfo = forge.getForgery(), + deviceInfo = forge.getForgery(), + userInfo = forge.getForgery(), + accountInfo = forge.getForgery(), + trackingConsent = forge.aValueFrom(TrackingConsent::class.java), + appBuildId = forge.aNullable { getForgery().toString() }, + // building nested maps with default size slows down tests quite a lot, so will use + // an explicit small size + featuresContext = forge.aMap(size = 2) { + forge.anAlphabeticalString() to forge.exhaustiveAttributes() + } + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DeviceInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DeviceInfoForgeryFactory.kt new file mode 100644 index 0000000000..eee7cca114 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DeviceInfoForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.context.DeviceInfo +import com.datadog.android.api.context.DeviceType +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class DeviceInfoForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): DeviceInfo { + return DeviceInfo( + deviceName = forge.anAlphabeticalString(), + deviceBrand = forge.anAlphabeticalString(), + deviceModel = forge.anAlphabeticalString(), + deviceType = forge.aValueFrom(DeviceType::class.java), + deviceBuildId = forge.anAlphaNumericalString(), + osName = forge.aString(), + osVersion = forge.aString(), + osMajorVersion = forge.aString(), + architecture = forge.aString(), + numberOfDisplays = forge.aNullable { forge.anInt() }, + localeInfo = forge.getForgery() + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt new file mode 100644 index 0000000000..77f9f8639c --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +fun Forge.exhaustiveAttributes( + excludedKeys: Set = emptySet() +): MutableMap { + return listOf( + aBool(), + anInt(), + aLong(), + aFloat(), + aDouble(), + anAsciiString(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + aList { anAlphabeticalString() }, + aList { aDouble() }, + null + ) + .associateBy { anAlphaNumericalString() } + .filter { it.key !in excludedKeys } + .toMutableMap() +} + +fun T.useCoreFactories(): T { + addFactory(DatadogContextForgeryFactory()) + addFactory(DeviceInfoForgeryFactory()) + addFactory(NetworkInfoForgeryFactory()) + addFactory(LocaleInfoForgeryFactory()) + addFactory(ProcessInfoForgeryFactory()) + addFactory(TimeInfoForgeryFactory()) + addFactory(UserInfoForgeryFactory()) + addFactory(AccountInfoForgeryFactory()) + addFactory(RawBatchEventForgeryFactory()) + addFactory(ThreadDumpForgeryFactory()) + addFactory(RequestExecutionContextForgeryFactory()) + + return this +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/LocaleInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/LocaleInfoForgeryFactory.kt new file mode 100644 index 0000000000..715042d099 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/LocaleInfoForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.context.LocaleInfo +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class LocaleInfoForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): LocaleInfo { + return LocaleInfo( + locales = forge.aList { forge.aString() }, + currentLocale = forge.aString(), + timeZone = forge.aString() + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/NetworkInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/NetworkInfoForgeryFactory.kt new file mode 100644 index 0000000000..7beaeaed08 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/NetworkInfoForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.context.NetworkInfo +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class NetworkInfoForgeryFactory : ForgeryFactory { + + @Suppress("MagicNumber") + override fun getForgery(forge: Forge): NetworkInfo { + return NetworkInfo( + connectivity = forge.aValueFrom(NetworkInfo.Connectivity::class.java), + carrierName = forge.anElementFrom( + forge.anAlphabeticalString(), + forge.aWhitespaceString(), + null + ), + carrierId = forge.aNullable { forge.aLong(0, 10000) }, + upKbps = forge.aNullable { forge.aLong(1, Long.MAX_VALUE) }, + downKbps = forge.aNullable { forge.aLong(1, Long.MAX_VALUE) }, + strength = forge.aNullable { forge.aLong(-100, -30) }, // dBm for wifi signal + cellularTechnology = forge.aNullable { anAlphabeticalString() } + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ProcessInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ProcessInfoForgeryFactory.kt new file mode 100644 index 0000000000..9ef357e698 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ProcessInfoForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.context.ProcessInfo +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class ProcessInfoForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): ProcessInfo { + return ProcessInfo( + isMainProcess = forge.aBool() + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RawBatchEventForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RawBatchEventForgeryFactory.kt new file mode 100644 index 0000000000..2b410c95ac --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RawBatchEventForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.storage.RawBatchEvent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class RawBatchEventForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): RawBatchEvent { + return RawBatchEvent( + data = forge.aString(forge.anInt(1, EVENT_MAX_SIZE + 1)).toByteArray(), + metadata = forge.aString(forge.anInt(0, METADATA_MAX_SIZE + 1)).toByteArray() + ) + } + + companion object { + const val EVENT_MAX_SIZE = 512 + const val METADATA_MAX_SIZE = 32 + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RequestExecutionContextForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RequestExecutionContextForgeryFactory.kt new file mode 100644 index 0000000000..57a15a89a1 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RequestExecutionContextForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.net.RequestExecutionContext +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class RequestExecutionContextForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): RequestExecutionContext { + return RequestExecutionContext( + attemptNumber = forge.aPositiveInt(), + previousResponseCode = forge.aNullable { aPositiveInt() } + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ThreadDumpForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ThreadDumpForgeryFactory.kt new file mode 100644 index 0000000000..2a6ae8d602 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ThreadDumpForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.core.feature.event.ThreadDump +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class ThreadDumpForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): ThreadDump { + return ThreadDump( + name = forge.anAlphaNumericalString(), + state = forge.getForgery().name.lowercase(), + crashed = forge.aBool(), + stack = forge.aList { + StackTraceElement( + // declaring class + forge.anAlphabeticalString(), + // method name + forge.anAlphabeticalString(), + // file name + forge.aNullable { anAlphabeticalString() + if (forge.aBool()) ".java" else ".kt" }, + // line number + // A value of -2 indicates that the method containing the execution point is a native method + forge.anInt(min = -2) + ) + }.toTypedArray().joinToString(separator = "\n") { "at $it" } + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/TimeInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/TimeInfoForgeryFactory.kt new file mode 100644 index 0000000000..0f8edef0e7 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/TimeInfoForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.context.TimeInfo +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class TimeInfoForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): TimeInfo { + return TimeInfo( + deviceTimeNs = forge.aLong(min = 0), + serverTimeNs = forge.aLong(min = 0), + serverTimeOffsetNs = forge.aLong(), + serverTimeOffsetMs = forge.aLong() + ) + } +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt new file mode 100644 index 0000000000..b159309620 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.tests.elmyr + +import com.datadog.android.api.context.UserInfo +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class UserInfoForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): UserInfo { + return UserInfo( + id = forge.aNullable { anHexadecimalString() }, + name = forge.aNullable { forge.aStringMatching("[A-Z][a-z]+ [A-Z]\\. [A-Z][a-z]+") }, + email = forge.aNullable { forge.aStringMatching("[a-z]+\\.[a-z]+@[a-z]+\\.[a-z]{3}") }, + additionalProperties = forge.exhaustiveAttributes(excludedKeys = setOf("id", "name", "email")) + ) + } +} diff --git a/dd-sdk-android-core/src/testRelease/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerReleaseTest.kt b/dd-sdk-android-core/src/testRelease/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerReleaseTest.kt new file mode 100644 index 0000000000..62094afc28 --- /dev/null +++ b/dd-sdk-android-core/src/testRelease/kotlin/com/datadog/android/core/internal/logger/SdkInternalLoggerReleaseTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.logger + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock + +internal class SdkInternalLoggerReleaseTest { + + // region sdkLogger + + @Test + @Suppress("FunctionNaming", "FunctionMaxLength") + fun `M not build sdkLogger W init()`() { + // When + val logger = SdkInternalLogger( + sdkCore = mock() + ) + + // Then + assertThat(logger.maintainerLogger).isNull() + } + + // endregion +} diff --git a/dd-sdk-android-core/transitiveDependencies b/dd-sdk-android-core/transitiveDependencies new file mode 100644 index 0000000000..ddac0a46d1 --- /dev/null +++ b/dd-sdk-android-core/transitiveDependencies @@ -0,0 +1,25 @@ +Dependencies List + +androidx.annotation:annotation-experimental:1.1.0 : 16 Kb +androidx.annotation:annotation-jvm:1.9.1 : 59 Kb +androidx.arch.core:core-common:2.1.0 : 10 Kb +androidx.arch.core:core-runtime:2.1.0 : 5 Kb +androidx.collection:collection-jvm:1.4.5 : 770 Kb +androidx.lifecycle:lifecycle-common:2.1.0 : 21 Kb +androidx.lifecycle:lifecycle-livedata-core:2.1.0 : 8 Kb +androidx.lifecycle:lifecycle-livedata:2.1.0 : 10 Kb +androidx.startup:startup-runtime:1.0.0 : 18 Kb +androidx.work:work-runtime:2.8.1 : 1626 Kb +com.google.code.gson:gson:2.10.1 : 276 Kb +com.google.guava:listenablefuture:1.0 : 3 Kb +com.lyft.kronos:kronos-android:0.0.1-alpha11 : 5 Kb +com.lyft.kronos:kronos-java:0.0.1-alpha11 : 29 Kb +com.squareup.okhttp3:okhttp:4.12.0 : 771 Kb +com.squareup.okio:okio-jvm:3.6.0 : 351 Kb +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 : 959 b +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 : 965 b +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 : 1706 Kb +org.jetbrains:annotations:13.0 : 17 Kb + +Total transitive dependencies size : 5 Mb + diff --git a/dd-sdk-android-fresco/README.md b/dd-sdk-android-fresco/README.md deleted file mode 100644 index fece5cb270..0000000000 --- a/dd-sdk-android-fresco/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Datadog Integration for Fresco - -## Getting Started - -To include the Datadog integration for [Fresco][1] in your project, simply add the -following to your application's `build.gradle` file. - -``` -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:" - implementation "com.datadoghq:dd-sdk-android-fresco:" -} -``` - -### Initial Setup - -Before you can use the SDK, you need to setup the library with your application -context, your Client token and your Application ID. -To generate a Client token and an Application ID please check **UX Monitoring > RUM Applications > New Application** -in the Datadog dashboard. - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "").build() - Datadog.initialize(this, config) - - val monitor = RumMonitor.Builder().build() - GlobalRum.registerIfAbsent(monitor) - } -} -``` - -Following Fresco's [Generated API documentation][2], you need to create your own `OkHttpImagePipelineConfigFactory` by providing your own `OkHttpClient` (configured with `DatadogInterceptor`). You can also add an instance of `DatadogFrescoCacheListener` in your `DiskCacheConfig`. - -Doing so will automatically track Fresco's network requests (creating both APM Traces and RUM Resource events), and will also listen for disk cache errors (creating RUM Error events). - -```kotlin - val config = OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient) - .setMainDiskCacheConfig( - DiskCacheConfig.newBuilder(context) - .setCacheEventListener(DatadogFrescoCacheListener()) - .build() - ) - .build() - Fresco.initialize(context, config) -``` - - -## Contributing - -Pull requests are welcome, but please open an issue first 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/facebook/fresco -[2]: https://frescolib.org/docs/index.html diff --git a/dd-sdk-android-fresco/apiSurface b/dd-sdk-android-fresco/apiSurface deleted file mode 100644 index 6621d0337c..0000000000 --- a/dd-sdk-android-fresco/apiSurface +++ /dev/null @@ -1,10 +0,0 @@ -class com.datadog.android.fresco.DatadogFrescoCacheListener : com.facebook.cache.common.CacheEventListener - override fun onMiss(com.facebook.cache.common.CacheEvent) - override fun onReadException(com.facebook.cache.common.CacheEvent) - override fun onEviction(com.facebook.cache.common.CacheEvent) - override fun onHit(com.facebook.cache.common.CacheEvent) - override fun onCleared() - override fun onWriteAttempt(com.facebook.cache.common.CacheEvent) - override fun onWriteSuccess(com.facebook.cache.common.CacheEvent) - override fun onWriteException(com.facebook.cache.common.CacheEvent) - companion object diff --git a/dd-sdk-android-fresco/build.gradle.kts b/dd-sdk-android-fresco/build.gradle.kts deleted file mode 100644 index d86819cb0a..0000000000 --- a/dd-sdk-android-fresco/build.gradle.kts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-2019 Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.implementation -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - id("androidx.benchmark") - kotlin("android") - kotlin("android.extensions") - kotlin("kapt") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - } -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.OkHttp) - implementation(Dependencies.Libraries.Fresco) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-fresco/src/main/AndroidManifest.xml b/dd-sdk-android-fresco/src/main/AndroidManifest.xml deleted file mode 100644 index dc014e2e84..0000000000 --- a/dd-sdk-android-fresco/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/dd-sdk-android-fresco/src/test/kotlin/com/datadog/android/fresco/utils/Configurator.kt b/dd-sdk-android-fresco/src/test/kotlin/com/datadog/android/fresco/utils/Configurator.kt deleted file mode 100644 index 43aa713cfa..0000000000 --- a/dd-sdk-android-fresco/src/test/kotlin/com/datadog/android/fresco/utils/Configurator.kt +++ /dev/null @@ -1,19 +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 2016-2020 Datadog, Inc. - */ - -package com.datadog.android.fresco.utils - -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeConfigurator -import fr.xgouchet.elmyr.jvm.useJvmFactories - -internal class Configurator : - ForgeConfigurator { - override fun configure(forge: Forge) { - forge.addFactory(ThrowableForgeryFactory()) - forge.useJvmFactories() - } -} diff --git a/dd-sdk-android-fresco/src/test/kotlin/com/datadog/android/fresco/utils/ThrowableForgeryFactory.kt b/dd-sdk-android-fresco/src/test/kotlin/com/datadog/android/fresco/utils/ThrowableForgeryFactory.kt deleted file mode 100644 index 28b87657f8..0000000000 --- a/dd-sdk-android-fresco/src/test/kotlin/com/datadog/android/fresco/utils/ThrowableForgeryFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.datadog.android.fresco.utils - -import com.datadog.tools.unit.forge.aThrowable -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeryFactory - -class ThrowableForgeryFactory : - ForgeryFactory { - override fun getForgery(forge: Forge): Throwable { - return forge.aThrowable() - } -} diff --git a/dd-sdk-android-fresco/transitiveDependencies b/dd-sdk-android-fresco/transitiveDependencies deleted file mode 100644 index 48ecd89d3b..0000000000 --- a/dd-sdk-android-fresco/transitiveDependencies +++ /dev/null @@ -1,23 +0,0 @@ -Dependencies List - -com.facebook.fresco:drawee:2.3.0 : 133 Kb -com.facebook.fresco:fbcore:2.3.0 : 122 Kb -com.facebook.fresco:fresco:2.3.0 : 47 Kb -com.facebook.fresco:imagepipeline-base:2.3.0 : 138 Kb -com.facebook.fresco:imagepipeline-native:2.3.0 : 23 Kb -com.facebook.fresco:imagepipeline-okhttp3:2.3.0 : 8 Kb -com.facebook.fresco:imagepipeline:2.3.0 : 402 Kb -com.facebook.fresco:memory-type-ashmem:2.3.0 : 4 Kb -com.facebook.fresco:memory-type-java:2.3.0 : 3 Kb -com.facebook.fresco:memory-type-native:2.3.0 : 4 Kb -com.facebook.fresco:nativeimagefilters:2.3.0 : 52 Kb -com.facebook.fresco:nativeimagetranscoder:2.3.0 : 764 Kb -com.squareup.okhttp3:okhttp:3.12.6 : 413 Kb -com.squareup.okio:okio:1.15.0 : 86 Kb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains:annotations:13.0 : 17 Kb - -Total transitive dependencies size : 3 Mb - diff --git a/dd-sdk-android-glide/README.md b/dd-sdk-android-glide/README.md deleted file mode 100644 index 367a285d6a..0000000000 --- a/dd-sdk-android-glide/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# Datadog Integration for Glide - -## Getting Started - -To include the Datadog integration for [Glide][1] in your project, simply add the -following to your application's `build.gradle` file. - -``` -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:" - implementation "com.datadoghq:dd-sdk-android-glide:" -} -``` - -### Initial Setup - -Before you can use the SDK, you need to setup the library with your application -context, your Client token and your Application ID. -To generate a Client token and an Application ID please check **UX Monitoring > RUM Applications > New Application** -in the Datadog dashboard. - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "").build() - Datadog.initialize(this, config) - - val monitor = RumMonitor.Builder().build() - GlobalRum.registerIfAbsent(monitor) - } -} -``` - -Following Glide's [Generated API documentation][2], you then need to create your own `GlideAppModule` with Datadog integrations by extending the `DatadogGlideModule`, as follow. - -Doing so will automatically track Glide's network requests (creating both APM Traces and RUM Resource events), and will also listen for disk cache and image transformation errors (creating RUM Error events). - -```kotlin -@GlideModule -class CustomGlideModule : - DatadogGlideModule( - listOf("example.com", "example.eu") - ) -``` - - -## Contributing - -Pull requests are welcome, but please open an issue first 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://bumptech.github.io/glide/ -[2]: https://bumptech.github.io/glide/doc/generatedapi.html \ No newline at end of file diff --git a/dd-sdk-android-glide/apiSurface b/dd-sdk-android-glide/apiSurface deleted file mode 100644 index 83a30ca180..0000000000 --- a/dd-sdk-android-glide/apiSurface +++ /dev/null @@ -1,8 +0,0 @@ -open class com.datadog.android.glide.DatadogGlideModule : com.bumptech.glide.module.AppGlideModule - constructor(List) - override fun registerComponents(android.content.Context, com.bumptech.glide.Glide, com.bumptech.glide.Registry) - override fun applyOptions(android.content.Context, com.bumptech.glide.GlideBuilder) - open fun getClientBuilder(): okhttp3.OkHttpClient.Builder -class com.datadog.android.glide.DatadogRUMUncaughtThrowableStrategy : com.bumptech.glide.load.engine.executor.GlideExecutor.UncaughtThrowableStrategy - constructor(String) - override fun handle(Throwable?) diff --git a/dd-sdk-android-glide/build.gradle.kts b/dd-sdk-android-glide/build.gradle.kts deleted file mode 100644 index f44e7dd530..0000000000 --- a/dd-sdk-android-glide/build.gradle.kts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-2019 Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.implementation -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - id("androidx.benchmark") - kotlin("android") - kotlin("android.extensions") - kotlin("kapt") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - isIgnoreTestSources = true - } -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.OkHttp) - implementation(Dependencies.Libraries.Glide) - - kapt(Dependencies.AnnotationProcessors.Glide) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-glide/src/main/AndroidManifest.xml b/dd-sdk-android-glide/src/main/AndroidManifest.xml deleted file mode 100644 index d6b71739b1..0000000000 --- a/dd-sdk-android-glide/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogGlideModule.kt b/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogGlideModule.kt deleted file mode 100644 index 4e026060d7..0000000000 --- a/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogGlideModule.kt +++ /dev/null @@ -1,76 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.glide - -import android.content.Context -import com.bumptech.glide.Glide -import com.bumptech.glide.GlideBuilder -import com.bumptech.glide.Registry -import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader -import com.bumptech.glide.load.engine.executor.GlideExecutor.newDiskCacheBuilder -import com.bumptech.glide.load.engine.executor.GlideExecutor.newSourceBuilder -import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.module.AppGlideModule -import com.datadog.android.DatadogEventListener -import com.datadog.android.DatadogInterceptor -import java.io.InputStream -import okhttp3.OkHttpClient - -/** - * Provides a basic implementation of [AppGlideModule] already set up to send relevant information - * to Datadog. - * - * This sets up an OkHttp based downloader that will send Traces and RUM Resource events. - * Also any Glide related error (Disk cache, source transformation, …) will be sent as RUM Errors. - */ -open class DatadogGlideModule( - private val tracedHosts: List -) : AppGlideModule() { - - // region AppGlideModule - - /** @inheritdoc */ - override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - val client = getClientBuilder().build() - val factory = OkHttpUrlLoader.Factory(client) - - registry.replace(GlideUrl::class.java, InputStream::class.java, factory) - } - - /** @inheritdoc */ - override fun applyOptions(context: Context, builder: GlideBuilder) { - builder.setDiskCacheExecutor( - newDiskCacheBuilder() - .setUncaughtThrowableStrategy(DatadogRUMUncaughtThrowableStrategy("Disk Cache")) - .build() - ) - - builder.setSourceExecutor( - newSourceBuilder() - .setUncaughtThrowableStrategy(DatadogRUMUncaughtThrowableStrategy("Source")) - .build() - ) - } - - // endregion - - // region DatadogGlideModule - - /** - * Creates the [OkHttpClient.Builder]. - * The default implementation returns a builder already setup with a [DatadogInterceptor] - * and [DatadogEventListener.Factory]. - * @return the builder for the [OkHttpClient] to be used by Glide - */ - open fun getClientBuilder(): OkHttpClient.Builder { - return OkHttpClient.Builder() - .addInterceptor((DatadogInterceptor(tracedHosts))) - .eventListenerFactory(DatadogEventListener.Factory()) - } - - // endregion -} diff --git a/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategy.kt b/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategy.kt deleted file mode 100644 index 13ae1a09ba..0000000000 --- a/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategy.kt +++ /dev/null @@ -1,36 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.glide - -import com.bumptech.glide.load.engine.executor.GlideExecutor -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor - -/** - * A [GlideExecutor.UncaughtThrowableStrategy] implementation that will forward all errors - * to the active [RumMonitor]. - * - * @param name the name of the feature this strategy will be used for - * (e.g.: "Disk Cache", "Source", …) - */ -class DatadogRUMUncaughtThrowableStrategy( - val name: String -) : GlideExecutor.UncaughtThrowableStrategy { - - // region GlideExecutor.UncaughtThrowableStrategy - - /** @inheritdoc */ - override fun handle(t: Throwable?) { - if (t != null) { - GlobalRum.get() - .addError("Glide $name error", RumErrorSource.SOURCE, t, emptyMap()) - } - } - - // endregion -} diff --git a/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt b/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt deleted file mode 100644 index 8620734205..0000000000 --- a/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.glide - -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.tools.unit.getStaticValue -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.lang.RuntimeException -import java.util.concurrent.atomic.AtomicBoolean -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class DatadogRUMUncaughtThrowableStrategyTest { - - lateinit var testedStrategy: DatadogRUMUncaughtThrowableStrategy - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @StringForgery - lateinit var fakeName: String - - @BeforeEach - fun `set up`() { - GlobalRum.registerIfAbsent(mockRumMonitor) - - testedStrategy = DatadogRUMUncaughtThrowableStrategy(fakeName) - } - - @AfterEach - fun `tear down`() { - val isRegistered: AtomicBoolean = GlobalRum::class.java.getStaticValue("isRegistered") - isRegistered.set(false) - } - - @Test - fun `handles throwable`( - @StringForgery message: String - ) { - val throwable = RuntimeException(message) - - testedStrategy.handle(throwable) - - verify(mockRumMonitor) - .addError("Glide $fakeName error", RumErrorSource.SOURCE, throwable, emptyMap()) - } - - @Test - fun `handles null throwable`() { - testedStrategy.handle(null) - - verifyZeroInteractions(mockRumMonitor) - } -} diff --git a/dd-sdk-android-glide/transitiveDependencies b/dd-sdk-android-glide/transitiveDependencies deleted file mode 100644 index bdfe974e35..0000000000 --- a/dd-sdk-android-glide/transitiveDependencies +++ /dev/null @@ -1,49 +0,0 @@ -Dependencies List - -androidx.activity:activity:1.1.0 : 13 Kb -androidx.annotation:annotation:1.1.0 : 27 Kb -androidx.arch.core:core-common:2.1.0 : 10 Kb -androidx.arch.core:core-runtime:2.1.0 : 5 Kb -androidx.asynclayoutinflater:asynclayoutinflater:1.0.0 : 7 Kb -androidx.collection:collection:1.1.0 : 41 Kb -androidx.coordinatorlayout:coordinatorlayout:1.0.0 : 43 Kb -androidx.core:core:1.3.1 : 710 Kb -androidx.cursoradapter:cursoradapter:1.0.0 : 10 Kb -androidx.customview:customview:1.0.0 : 32 Kb -androidx.documentfile:documentfile:1.0.0 : 10 Kb -androidx.drawerlayout:drawerlayout:1.0.0 : 31 Kb -androidx.exifinterface:exifinterface:1.0.0 : 44 Kb -androidx.fragment:fragment:1.2.4 : 221 Kb -androidx.interpolator:interpolator:1.0.0 : 7 Kb -androidx.legacy:legacy-support-core-ui:1.0.0 : 11 Kb -androidx.legacy:legacy-support-core-utils:1.0.0 : 4 Kb -androidx.lifecycle:lifecycle-common:2.2.0 : 21 Kb -androidx.lifecycle:lifecycle-livedata-core:2.2.0 : 8 Kb -androidx.lifecycle:lifecycle-livedata:2.1.0 : 10 Kb -androidx.lifecycle:lifecycle-runtime:2.2.0 : 10 Kb -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0 : 13 Kb -androidx.lifecycle:lifecycle-viewmodel:2.2.0 : 9 Kb -androidx.loader:loader:1.0.0 : 32 Kb -androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 : 6 Kb -androidx.print:print:1.0.0 : 15 Kb -androidx.savedstate:savedstate:1.0.0 : 9 Kb -androidx.slidingpanelayout:slidingpanelayout:1.0.0 : 22 Kb -androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 : 32 Kb -androidx.vectordrawable:vectordrawable-animated:1.0.0 : 33 Kb -androidx.vectordrawable:vectordrawable:1.0.0 : 31 Kb -androidx.versionedparcelable:versionedparcelable:1.1.0 : 30 Kb -androidx.viewpager:viewpager:1.0.0 : 52 Kb -com.github.bumptech.glide:annotations:4.11.0 : 3 Kb -com.github.bumptech.glide:disklrucache:4.11.0 : 19 Kb -com.github.bumptech.glide:gifdecoder:4.11.0 : 17 Kb -com.github.bumptech.glide:glide:4.11.0 : 614 Kb -com.github.bumptech.glide:okhttp3-integration:4.11.0 : 8 Kb -com.squareup.okhttp3:okhttp:3.12.6 : 413 Kb -com.squareup.okio:okio:1.15.0 : 86 Kb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains:annotations:13.0 : 17 Kb - -Total transitive dependencies size : 4 Mb - diff --git a/dd-sdk-android-internal/.gitignore b/dd-sdk-android-internal/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/dd-sdk-android-internal/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/dd-sdk-android-internal/api/apiSurface b/dd-sdk-android-internal/api/apiSurface new file mode 100644 index 0000000000..bf285d8cea --- /dev/null +++ b/dd-sdk-android-internal/api/apiSurface @@ -0,0 +1,139 @@ +interface com.datadog.android.internal.attributes.LocalAttribute + enum Key + constructor(String) + - CREATION_SAMPLING_RATE + - REPORTING_SAMPLING_RATE + - VIEW_SCOPE_INSTRUMENTATION_TYPE + override fun toString(): String + interface Constant + val key: Key +fun MutableMap.enrichWithConstantAttribute(LocalAttribute.Constant) +fun MutableMap.enrichWithNonNullAttribute(LocalAttribute.Key, Any?) +fun MutableMap.enrichWithLocalAttribute(LocalAttribute.Key, Any?) +enum com.datadog.android.internal.attributes.ViewScopeInstrumentationType : LocalAttribute.Constant + - MANUAL + - COMPOSE + - ACTIVITY + - FRAGMENT + override val key: LocalAttribute.Key +class com.datadog.android.internal.collections.EvictingQueue : java.util.Queue + constructor(Int = Int.MAX_VALUE) + override val size: Int + override fun add(T): Boolean + override fun offer(T): Boolean + override fun addAll(Collection): Boolean +enum com.datadog.android.internal.network.GraphQLHeaders + constructor(String) + - DD_GRAPHQL_NAME_HEADER + - DD_GRAPHQL_VARIABLES_HEADER + - DD_GRAPHQL_TYPE_HEADER + - DD_GRAPHQL_PAYLOAD_HEADER +interface com.datadog.android.internal.profiler.BenchmarkCounter + fun add(Long, Map) +interface com.datadog.android.internal.profiler.BenchmarkMeter + fun getCounter(String): BenchmarkCounter + fun createObservableGauge(String, Map, () -> Double) +interface com.datadog.android.internal.profiler.BenchmarkProfiler + fun getTracer(String): BenchmarkTracer +interface com.datadog.android.internal.profiler.BenchmarkSdkUploads + fun getMeter(String): BenchmarkMeter +interface com.datadog.android.internal.profiler.BenchmarkSpan + fun stop() +interface com.datadog.android.internal.profiler.BenchmarkSpanBuilder + fun startSpan(): BenchmarkSpan +fun withinBenchmarkSpan(String, Map = emptyMap(), BenchmarkSpan.() -> T): T +interface com.datadog.android.internal.profiler.BenchmarkTracer + fun spanBuilder(String, Map = emptyMap()): BenchmarkSpanBuilder +interface com.datadog.android.internal.profiler.ExecutionTimer + fun measure(() -> T): T +object com.datadog.android.internal.profiler.GlobalBenchmark + fun register(BenchmarkProfiler) + fun register(BenchmarkSdkUploads) + fun getProfiler(): BenchmarkProfiler + fun getBenchmarkSdkUploads(): BenchmarkSdkUploads + fun createExecutionTimer(String): ExecutionTimer +sealed class com.datadog.android.internal.telemetry.InternalTelemetryEvent + sealed class Log : InternalTelemetryEvent + constructor(String, Map?) + class Debug : Log + constructor(String, Map?) + class Error : Log + constructor(String, Map? = null, Throwable? = null, String? = null, String? = null) + fun resolveKind(): String? + fun resolveStacktrace(): String? + data class Configuration : InternalTelemetryEvent + constructor(Boolean, Long, Long, Boolean, Boolean, Int) + data class Metric : InternalTelemetryEvent + constructor(String, Map?) + sealed class ApiUsage : InternalTelemetryEvent + constructor(MutableMap = mutableMapOf()) + class AddViewLoadingTime : ApiUsage + constructor(Boolean, Boolean, Boolean, MutableMap = mutableMapOf()) + class AddOperationStepVital : ApiUsage + constructor(ActionType, MutableMap = mutableMapOf()) + enum ActionType + - START + - SUCCEED + - FAIL + object InterceptorInstantiated : InternalTelemetryEvent +enum com.datadog.android.internal.telemetry.TracingHeaderType + - DATADOG + - B3 + - B3MULTI + - TRACECONTEXT +data class com.datadog.android.internal.telemetry.TracingHeaderTypesSet + constructor(Set) +interface com.datadog.android.internal.thread.NamedExecutionUnit + val name: String +class com.datadog.android.internal.thread.NamedRunnable : NamedExecutionUnit, Runnable + constructor(String, Runnable) +class com.datadog.android.internal.thread.NamedCallable : NamedExecutionUnit, java.util.concurrent.Callable + constructor(String, java.util.concurrent.Callable) +class com.datadog.android.internal.time.DefaultTimeProvider : TimeProvider + override fun getDeviceTimestamp(): Long + override fun getServerTimestamp(): Long + override fun getServerOffsetNanos(): Long + override fun getServerOffsetMillis(): Long +interface com.datadog.android.internal.time.TimeProvider + fun getDeviceTimestamp(): Long + fun getServerTimestamp(): Long + fun getServerOffsetNanos(): Long + fun getServerOffsetMillis(): Long +fun ByteArray.toHexString(): String +interface com.datadog.android.internal.utils.DDCoreSubscription + fun addListener(T) + fun removeListener(T) + fun notifyListeners(T.() -> Unit) + val listenersCount: Int + companion object + fun create(): DDCoreSubscription +object com.datadog.android.internal.utils.ImageViewUtils + fun resolveParentRectAbsPosition(android.view.View, Boolean = true): android.graphics.Rect + fun calculateClipping(android.graphics.Rect, android.graphics.Rect, Float): android.graphics.Rect + fun resolveContentRectWithScaling(android.widget.ImageView, android.graphics.drawable.Drawable, android.widget.ImageView.ScaleType? = null): android.graphics.Rect +fun Int.densityNormalized(Float): Int +fun Long.densityNormalized(Float): Long +val NULL_MAP_VALUE: Object +fun Int.toHexString(): String +fun Long.toHexString(): String +fun java.math.BigInteger.toHexString(): String +fun allowThreadDiskReads(() -> T): T +fun allowThreadDiskWrites(() -> T): T +fun StringBuilder.appendIfNotEmpty(String) +fun StringBuilder.appendIfNotEmpty(Char) +fun Thread.safeGetThreadId(): Long +fun Thread.State.asString(): String +fun Array.loggableStackTrace(): String +fun Throwable.loggableStackTrace(): String +class com.datadog.android.rum.DdRumContentProvider : android.content.ContentProvider + override fun onCreate(): Boolean + override fun query(android.net.Uri, Array?, String?, Array?, String?): android.database.Cursor? + override fun getType(android.net.Uri): String? + override fun insert(android.net.Uri, android.content.ContentValues?): android.net.Uri? + override fun delete(android.net.Uri, String?, Array?): Int + override fun update(android.net.Uri, android.content.ContentValues?, String?, Array?): Int + companion object + var processImportance: Int + val createTimeNs: Long +annotation com.datadog.tools.annotation.NoOpImplementation + constructor(Boolean = false) diff --git a/dd-sdk-android-internal/api/dd-sdk-android-internal.api b/dd-sdk-android-internal/api/dd-sdk-android-internal.api new file mode 100644 index 0000000000..918e3629f0 --- /dev/null +++ b/dd-sdk-android-internal/api/dd-sdk-android-internal.api @@ -0,0 +1,341 @@ +public abstract interface class com/datadog/android/internal/attributes/LocalAttribute { +} + +public abstract interface class com/datadog/android/internal/attributes/LocalAttribute$Constant { + public abstract fun getKey ()Lcom/datadog/android/internal/attributes/LocalAttribute$Key; +} + +public final class com/datadog/android/internal/attributes/LocalAttribute$Key : java/lang/Enum { + public static final field CREATION_SAMPLING_RATE Lcom/datadog/android/internal/attributes/LocalAttribute$Key; + public static final field REPORTING_SAMPLING_RATE Lcom/datadog/android/internal/attributes/LocalAttribute$Key; + public static final field VIEW_SCOPE_INSTRUMENTATION_TYPE Lcom/datadog/android/internal/attributes/LocalAttribute$Key; + public fun toString ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/internal/attributes/LocalAttribute$Key; + public static fun values ()[Lcom/datadog/android/internal/attributes/LocalAttribute$Key; +} + +public final class com/datadog/android/internal/attributes/LocalAttributeKt { + public static final fun enrichWithConstantAttribute (Ljava/util/Map;Lcom/datadog/android/internal/attributes/LocalAttribute$Constant;)Ljava/util/Map; + public static final fun enrichWithLocalAttribute (Ljava/util/Map;Lcom/datadog/android/internal/attributes/LocalAttribute$Key;Ljava/lang/Object;)Ljava/util/Map; + public static final fun enrichWithNonNullAttribute (Ljava/util/Map;Lcom/datadog/android/internal/attributes/LocalAttribute$Key;Ljava/lang/Object;)Ljava/util/Map; +} + +public final class com/datadog/android/internal/attributes/ViewScopeInstrumentationType : java/lang/Enum, com/datadog/android/internal/attributes/LocalAttribute$Constant { + public static final field ACTIVITY Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; + public static final field COMPOSE Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; + public static final field FRAGMENT Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; + public static final field MANUAL Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; + public fun getKey ()Lcom/datadog/android/internal/attributes/LocalAttribute$Key; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; + public static fun values ()[Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; +} + +public final class com/datadog/android/internal/collections/EvictingQueue : java/util/Queue { + public fun (I)V + public synthetic fun (IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun add (Ljava/lang/Object;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public fun contains (Ljava/lang/Object;)Z + public fun containsAll (Ljava/util/Collection;)Z + public fun element ()Ljava/lang/Object; + public fun getSize ()I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public fun offer (Ljava/lang/Object;)Z + public fun peek ()Ljava/lang/Object; + public fun poll ()Ljava/lang/Object; + public fun remove ()Ljava/lang/Object; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun retainAll (Ljava/util/Collection;)Z + public final fun size ()I + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; +} + +public final class com/datadog/android/internal/network/GraphQLHeaders : java/lang/Enum { + public static final field DD_GRAPHQL_NAME_HEADER Lcom/datadog/android/internal/network/GraphQLHeaders; + public static final field DD_GRAPHQL_PAYLOAD_HEADER Lcom/datadog/android/internal/network/GraphQLHeaders; + public static final field DD_GRAPHQL_TYPE_HEADER Lcom/datadog/android/internal/network/GraphQLHeaders; + public static final field DD_GRAPHQL_VARIABLES_HEADER Lcom/datadog/android/internal/network/GraphQLHeaders; + public final fun getHeaderValue ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/internal/network/GraphQLHeaders; + public static fun values ()[Lcom/datadog/android/internal/network/GraphQLHeaders; +} + +public abstract interface class com/datadog/android/internal/profiler/BenchmarkCounter { + public abstract fun add (JLjava/util/Map;)V +} + +public abstract interface class com/datadog/android/internal/profiler/BenchmarkMeter { + public abstract fun createObservableGauge (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;)V + public abstract fun getCounter (Ljava/lang/String;)Lcom/datadog/android/internal/profiler/BenchmarkCounter; +} + +public abstract interface class com/datadog/android/internal/profiler/BenchmarkProfiler { + public abstract fun getTracer (Ljava/lang/String;)Lcom/datadog/android/internal/profiler/BenchmarkTracer; +} + +public abstract interface class com/datadog/android/internal/profiler/BenchmarkSdkUploads { + public abstract fun getMeter (Ljava/lang/String;)Lcom/datadog/android/internal/profiler/BenchmarkMeter; +} + +public abstract interface class com/datadog/android/internal/profiler/BenchmarkSpan { + public abstract fun stop ()V +} + +public abstract interface class com/datadog/android/internal/profiler/BenchmarkSpanBuilder { + public abstract fun startSpan ()Lcom/datadog/android/internal/profiler/BenchmarkSpan; +} + +public final class com/datadog/android/internal/profiler/BenchmarkSpanExtKt { + public static final fun withinBenchmarkSpan (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static synthetic fun withinBenchmarkSpan$default (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; +} + +public abstract interface class com/datadog/android/internal/profiler/BenchmarkTracer { + public abstract fun spanBuilder (Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/internal/profiler/BenchmarkSpanBuilder; +} + +public final class com/datadog/android/internal/profiler/BenchmarkTracer$DefaultImpls { + public static synthetic fun spanBuilder$default (Lcom/datadog/android/internal/profiler/BenchmarkTracer;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/internal/profiler/BenchmarkSpanBuilder; +} + +public abstract interface class com/datadog/android/internal/profiler/ExecutionTimer { + public abstract fun measure (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + +public final class com/datadog/android/internal/profiler/GlobalBenchmark { + public static final field INSTANCE Lcom/datadog/android/internal/profiler/GlobalBenchmark; + public final fun createExecutionTimer (Ljava/lang/String;)Lcom/datadog/android/internal/profiler/ExecutionTimer; + public final fun getBenchmarkSdkUploads ()Lcom/datadog/android/internal/profiler/BenchmarkSdkUploads; + public final fun getProfiler ()Lcom/datadog/android/internal/profiler/BenchmarkProfiler; + public final fun register (Lcom/datadog/android/internal/profiler/BenchmarkProfiler;)V + public final fun register (Lcom/datadog/android/internal/profiler/BenchmarkSdkUploads;)V +} + +public abstract class com/datadog/android/internal/telemetry/InternalTelemetryEvent { +} + +public abstract class com/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage : com/datadog/android/internal/telemetry/InternalTelemetryEvent { + public synthetic fun (Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAdditionalProperties ()Ljava/util/Map; +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital : com/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage { + public fun (Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType;Ljava/util/Map;)V + public synthetic fun (Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getActionType ()Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType; +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType : java/lang/Enum { + public static final field FAIL Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType; + public static final field START Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType; + public static final field SUCCEED Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType; + public static fun values ()[Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddOperationStepVital$ActionType; +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$AddViewLoadingTime : com/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage { + public fun (ZZZLjava/util/Map;)V + public synthetic fun (ZZZLjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getNoActiveView ()Z + public final fun getNoView ()Z + public final fun getOverwrite ()Z +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration : com/datadog/android/internal/telemetry/InternalTelemetryEvent { + public fun (ZJJZZI)V + public final fun component1 ()Z + public final fun component2 ()J + public final fun component3 ()J + public final fun component4 ()Z + public final fun component5 ()Z + public final fun component6 ()I + public final fun copy (ZJJZZI)Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration; + public static synthetic fun copy$default (Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;ZJJZZIILjava/lang/Object;)Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration; + public fun equals (Ljava/lang/Object;)Z + public final fun getBatchProcessingLevel ()I + public final fun getBatchSize ()J + public final fun getBatchUploadFrequency ()J + public final fun getTrackErrors ()Z + public final fun getUseLocalEncryption ()Z + public final fun getUseProxy ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated : com/datadog/android/internal/telemetry/InternalTelemetryEvent { + public static final field INSTANCE Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated; +} + +public abstract class com/datadog/android/internal/telemetry/InternalTelemetryEvent$Log : com/datadog/android/internal/telemetry/InternalTelemetryEvent { + public synthetic fun (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAdditionalProperties ()Ljava/util/Map; + public final fun getMessage ()Ljava/lang/String; +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$Log$Debug : com/datadog/android/internal/telemetry/InternalTelemetryEvent$Log { + public fun (Ljava/lang/String;Ljava/util/Map;)V +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$Log$Error : com/datadog/android/internal/telemetry/InternalTelemetryEvent$Log { + public fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getError ()Ljava/lang/Throwable; + public final fun getKind ()Ljava/lang/String; + public final fun getStacktrace ()Ljava/lang/String; + public final fun resolveKind ()Ljava/lang/String; + public final fun resolveStacktrace ()Ljava/lang/String; +} + +public final class com/datadog/android/internal/telemetry/InternalTelemetryEvent$Metric : com/datadog/android/internal/telemetry/InternalTelemetryEvent { + public fun (Ljava/lang/String;Ljava/util/Map;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Metric; + public static synthetic fun copy$default (Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Metric;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Metric; + public fun equals (Ljava/lang/Object;)Z + public final fun getAdditionalProperties ()Ljava/util/Map; + public final fun getMessage ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/internal/telemetry/TracingHeaderType : java/lang/Enum { + public static final field B3 Lcom/datadog/android/internal/telemetry/TracingHeaderType; + public static final field B3MULTI Lcom/datadog/android/internal/telemetry/TracingHeaderType; + public static final field DATADOG Lcom/datadog/android/internal/telemetry/TracingHeaderType; + public static final field TRACECONTEXT Lcom/datadog/android/internal/telemetry/TracingHeaderType; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/internal/telemetry/TracingHeaderType; + public static fun values ()[Lcom/datadog/android/internal/telemetry/TracingHeaderType; +} + +public final class com/datadog/android/internal/telemetry/TracingHeaderTypesSet { + public fun (Ljava/util/Set;)V + public final fun component1 ()Ljava/util/Set; + public final fun copy (Ljava/util/Set;)Lcom/datadog/android/internal/telemetry/TracingHeaderTypesSet; + public static synthetic fun copy$default (Lcom/datadog/android/internal/telemetry/TracingHeaderTypesSet;Ljava/util/Set;ILjava/lang/Object;)Lcom/datadog/android/internal/telemetry/TracingHeaderTypesSet; + public fun equals (Ljava/lang/Object;)Z + public final fun getTypes ()Ljava/util/Set; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/internal/thread/NamedCallable : com/datadog/android/internal/thread/NamedExecutionUnit, java/util/concurrent/Callable { + public fun (Ljava/lang/String;Ljava/util/concurrent/Callable;)V + public fun call ()Ljava/lang/Object; + public fun getName ()Ljava/lang/String; +} + +public abstract interface class com/datadog/android/internal/thread/NamedExecutionUnit { + public abstract fun getName ()Ljava/lang/String; +} + +public final class com/datadog/android/internal/thread/NamedRunnable : com/datadog/android/internal/thread/NamedExecutionUnit, java/lang/Runnable { + public fun (Ljava/lang/String;Ljava/lang/Runnable;)V + public fun getName ()Ljava/lang/String; + public fun run ()V +} + +public final class com/datadog/android/internal/time/DefaultTimeProvider : com/datadog/android/internal/time/TimeProvider { + public fun ()V + public fun getDeviceTimestamp ()J + public fun getServerOffsetMillis ()J + public fun getServerOffsetNanos ()J + public fun getServerTimestamp ()J +} + +public abstract interface class com/datadog/android/internal/time/TimeProvider { + public abstract fun getDeviceTimestamp ()J + public abstract fun getServerOffsetMillis ()J + public abstract fun getServerOffsetNanos ()J + public abstract fun getServerTimestamp ()J +} + +public final class com/datadog/android/internal/utils/ByteArrayExtKt { + public static final fun toHexString ([B)Ljava/lang/String; +} + +public abstract interface class com/datadog/android/internal/utils/DDCoreSubscription { + public static final field Companion Lcom/datadog/android/internal/utils/DDCoreSubscription$Companion; + public abstract fun addListener (Ljava/lang/Object;)V + public abstract fun getListenersCount ()I + public abstract fun notifyListeners (Lkotlin/jvm/functions/Function1;)V + public abstract fun removeListener (Ljava/lang/Object;)V +} + +public final class com/datadog/android/internal/utils/DDCoreSubscription$Companion { + public final fun create ()Lcom/datadog/android/internal/utils/DDCoreSubscription; +} + +public final class com/datadog/android/internal/utils/ImageViewUtils { + public static final field INSTANCE Lcom/datadog/android/internal/utils/ImageViewUtils; + public final fun calculateClipping (Landroid/graphics/Rect;Landroid/graphics/Rect;F)Landroid/graphics/Rect; + public final fun resolveContentRectWithScaling (Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;Landroid/widget/ImageView$ScaleType;)Landroid/graphics/Rect; + public static synthetic fun resolveContentRectWithScaling$default (Lcom/datadog/android/internal/utils/ImageViewUtils;Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;Landroid/widget/ImageView$ScaleType;ILjava/lang/Object;)Landroid/graphics/Rect; + public final fun resolveParentRectAbsPosition (Landroid/view/View;Z)Landroid/graphics/Rect; + public static synthetic fun resolveParentRectAbsPosition$default (Lcom/datadog/android/internal/utils/ImageViewUtils;Landroid/view/View;ZILjava/lang/Object;)Landroid/graphics/Rect; +} + +public final class com/datadog/android/internal/utils/IntExtKt { + public static final fun densityNormalized (IF)I +} + +public final class com/datadog/android/internal/utils/LongExtKt { + public static final fun densityNormalized (JF)J +} + +public final class com/datadog/android/internal/utils/MapUtilsKt { + public static final fun getNULL_MAP_VALUE ()Ljava/lang/Object; +} + +public final class com/datadog/android/internal/utils/NumberExtKt { + public static final fun toHexString (I)Ljava/lang/String; + public static final fun toHexString (J)Ljava/lang/String; + public static final fun toHexString (Ljava/math/BigInteger;)Ljava/lang/String; +} + +public final class com/datadog/android/internal/utils/StrictModeExtKt { + public static final fun allowThreadDiskReads (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public static final fun allowThreadDiskWrites (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + +public final class com/datadog/android/internal/utils/StringBuilderExtKt { + public static final fun appendIfNotEmpty (Ljava/lang/StringBuilder;C)Ljava/lang/StringBuilder; + public static final fun appendIfNotEmpty (Ljava/lang/StringBuilder;Ljava/lang/String;)Ljava/lang/StringBuilder; +} + +public final class com/datadog/android/internal/utils/ThreadExtKt { + public static final fun asString (Ljava/lang/Thread$State;)Ljava/lang/String; + public static final fun loggableStackTrace ([Ljava/lang/StackTraceElement;)Ljava/lang/String; + public static final fun safeGetThreadId (Ljava/lang/Thread;)J +} + +public final class com/datadog/android/internal/utils/ThrowableExtKt { + public static final fun loggableStackTrace (Ljava/lang/Throwable;)Ljava/lang/String; +} + +public final class com/datadog/android/rum/DdRumContentProvider : android/content/ContentProvider { + public static final field Companion Lcom/datadog/android/rum/DdRumContentProvider$Companion; + public fun ()V + public fun delete (Landroid/net/Uri;Ljava/lang/String;[Ljava/lang/String;)I + public fun getType (Landroid/net/Uri;)Ljava/lang/String; + public fun insert (Landroid/net/Uri;Landroid/content/ContentValues;)Landroid/net/Uri; + public fun onCreate ()Z + public fun query (Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor; + public fun update (Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I +} + +public final class com/datadog/android/rum/DdRumContentProvider$Companion { + public final fun getCreateTimeNs ()J + public final fun getProcessImportance ()I + public final fun setProcessImportance (I)V +} + +public abstract interface annotation class com/datadog/tools/annotation/NoOpImplementation : java/lang/annotation/Annotation { + public abstract fun publicNoOpImplementation ()Z +} + diff --git a/dd-sdk-android-internal/build.gradle.kts b/dd-sdk-android-internal/build.gradle.kts new file mode 100644 index 0000000000..b53c374849 --- /dev/null +++ b/dd-sdk-android-internal/build.gradle.kts @@ -0,0 +1,105 @@ +import com.datadog.gradle.config.androidLibraryConfig +import com.datadog.gradle.config.dependencyUpdateConfig +import com.datadog.gradle.config.detektCustomConfig +import com.datadog.gradle.config.java11 +import com.datadog.gradle.config.javadocConfig +import com.datadog.gradle.config.junitConfig +import com.datadog.gradle.config.kotlinConfig +import com.datadog.gradle.config.publishingConfig +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.library") + kotlin("android") + id("com.google.devtools.ksp") + + // Publish + `maven-publish` + signing + id("org.jetbrains.dokka-javadoc") + + // Analysis tools + id("com.github.ben-manes.versions") + + // Tests + id("de.mobilej.unmock") + id("org.jetbrains.kotlinx.kover") + + // Internal Generation + id("com.datadoghq.dependency-license") + id("apiSurface") + id("transitiveDependencies") + id("verificationXml") + id("binary-compatibility-validator") +} + +android { + namespace = "com.datadog.android.internal" + compileOptions { + java11() + } + + testFixtures { + enable = true + } +} + +dependencies { + implementation(libs.kotlin) + + // Generate NoOp implementations + ksp(project(":tools:noopfactory")) + testImplementation(project(":tools:unit")) { + attributes { + attribute( + com.android.build.api.attributes.ProductFlavorAttr.of("platform"), + objects.named("jvm") + ) + } + } + testImplementation(libs.bundles.jUnit5) + testImplementation(libs.bundles.testTools) + testFixturesImplementation(libs.kotlin) + testFixturesImplementation(libs.bundles.jUnit5) + testFixturesImplementation(libs.bundles.testTools) + testFixturesImplementation(project(":tools:unit")) { + attributes { + attribute( + com.android.build.api.attributes.ProductFlavorAttr.of("platform"), + objects.named("jvm") + ) + } + } + unmock(libs.robolectric) +} + +kotlinConfig(jvmBytecodeTarget = JvmTarget.JVM_11) +androidLibraryConfig() +junitConfig() +javadocConfig() +dependencyUpdateConfig() +publishingConfig( + "Internal library to be used by the Datadog SDK modules." +) +detektCustomConfig() + +unMock { + keep("android.os.BaseBundle") + keep("android.os.Bundle") + keep("android.os.Parcel") + keepStartingWith("com.android.internal.util.") + keepStartingWith("android.util.") + keep("android.content.ComponentName") + keep("android.content.ContentProvider") + keep("android.content.IContentProvider") + keep("android.content.ContentProviderNative") + keep("android.net.Uri") + keep("android.os.Handler") + keep("android.os.IMessenger") + keep("android.os.Looper") + keep("android.os.Message") + keep("android.os.MessageQueue") + keep("android.os.SystemProperties") + keep("android.view.DisplayEventReceiver") + keepStartingWith("org.json") +} diff --git a/dd-sdk-android-internal/src/main/AndroidManifest.xml b/dd-sdk-android-internal/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..3a5ce7d91f --- /dev/null +++ b/dd-sdk-android-internal/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/attributes/LocalAttribute.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/attributes/LocalAttribute.kt new file mode 100644 index 0000000000..09822b066a --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/attributes/LocalAttribute.kt @@ -0,0 +1,97 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +package com.datadog.android.internal.attributes + +/** + * Local attributes are used to pass additional metadata along with the event + * and are never sent to the backend directly. + */ +interface LocalAttribute { + + /** + * Enumeration of all local attributes keys used in the application. + * Made via **enum** to make sure that all such attributes will be removed before sending the event to the backend. + * + * @param string - Unique string value for a local attribute key. + */ + enum class Key( + private val string: String + ) { + /** + * Some of the metrics such as [PerformanceMetric] are sampled at the point of + * metric creation and then reported with 100% probability. + * In such cases we need to use *creationSampleRate* to correctly calculate effectiveSampleRate. + * The creation(head) sample rate only exists for long-lived metrics such as method performance. + * Created metric still could not be sent, it depends on the [REPORTING_SAMPLING_RATE] sample rate. + */ + CREATION_SAMPLING_RATE("_dd.local.head_sampling_rate_key"), + + /** + * Sampling rate that is used to decide to send or not to send the metric. + * Each metric should have reporting(tail) sampling rate. + * It's possible that metric has only reporting(tail) sampling rate. + */ + REPORTING_SAMPLING_RATE("_dd.local.tail_sampling_rate_key"), + + /** + * Indicates which instrumentation was used to track the view scope. + * See [ViewScopeInstrumentationType] for possible values. + */ + VIEW_SCOPE_INSTRUMENTATION_TYPE("_dd.local.view_instrumentation_type_key"); + + override fun toString(): String { + return string + } + } + + /** + * Used for attributes that have a finite set of possible values (such as enumerations, see [ViewScopeInstrumentationType]). + * This interface makes it possible to use only the value (see [enrichWithConstantAttribute]) when setting + * an attribute and reduces the possibility of inconsistent use of api (when an unsupported value is passed + * for a particular attribute key). + */ + interface Constant { + /** Constant attribute key. For enum constants will be same for all values. */ + val key: Key + } +} + +/** + * Adds local attribute to the mutable map. + * + * @param attribute - Constant attribute value that should be added. + * Key for the attribute will be resolved automatically. + */ +fun MutableMap.enrichWithConstantAttribute( + attribute: LocalAttribute.Constant +) = enrichWithLocalAttribute( + attribute.key, + attribute +) + +/** + * Adds value to the map for specified key if value is not null. + * + * @param key - local attribute key. + * @param value - attribute value. + */ +fun MutableMap.enrichWithNonNullAttribute( + key: LocalAttribute.Key, + value: Any? +) = value?.let { enrichWithLocalAttribute(key, it) } ?: this + +/** + * Adds value to the map for specified key. + * + * @param key - local attribute key. + * @param value - attribute value. + */ +fun MutableMap.enrichWithLocalAttribute( + key: LocalAttribute.Key, + value: Any? +) = apply { + this[key.toString()] = value +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/attributes/ViewScopeInstrumentationType.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/attributes/ViewScopeInstrumentationType.kt new file mode 100644 index 0000000000..633cc69c21 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/attributes/ViewScopeInstrumentationType.kt @@ -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 2016-Present Datadog, Inc. + */ +package com.datadog.android.internal.attributes + +/** + * A set of constants describing the instrumentation that were used to define the view scope. + */ +enum class ViewScopeInstrumentationType : LocalAttribute.Constant { + /** Tracked manually through the RUMMonitor API. */ + MANUAL, + + /** Tracked through ComposeNavigationObserver instrumentation. */ + COMPOSE, + + /** Tracked through ActivityViewTrackingStrategy instrumentation. */ + ACTIVITY, + + /** Tracked through FragmentViewTrackingStrategy instrumentation. */ + FRAGMENT; + + /** @inheritdoc */ + override val key: LocalAttribute.Key = LocalAttribute.Key.VIEW_SCOPE_INSTRUMENTATION_TYPE +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/collections/EvictingQueue.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/collections/EvictingQueue.kt new file mode 100644 index 0000000000..e5c29f68ba --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/collections/EvictingQueue.kt @@ -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 2016-Present Datadog, Inc. + */ +package com.datadog.android.internal.collections + +import java.util.LinkedList +import java.util.Queue +import kotlin.math.max + +/** + * A bounded queue that automatically evicts the oldest elements when new elements are added beyond its maximum capacity. + * + * This implementation delegates all [Queue] operations to an underlying [LinkedList]. It provides a FIFO (first-in, first-out) + * behavior with a fixed maximum size. When new elements are added and the queue is at capacity, the oldest element is evicted. + * + * @param T the type of elements held in this queue. + * @param maxSize the maximum number of elements the queue can hold. Must be greater than or equal to 0. + * The default value is [Int.MAX_VALUE], which effectively means there is no practical bound. + * @param delegate the underlying [LinkedList] that stores the elements and to which all [Queue] operations are delegated. + */ +class EvictingQueue private constructor( + maxSize: Int, + private val delegate: LinkedList +) : Queue by delegate { + + /** + * Secondary constructor that initializes the [EvictingQueue] with the given [maxSize]. + * + * @param maxSize the maximum number of elements the queue can hold. + */ + constructor(maxSize: Int = Int.MAX_VALUE) : this(maxSize, LinkedList()) + + override val size: Int + get() = delegate.size + private val maxSize: Int = max(0, maxSize) + + /** + * Adds the specified [element] to the end of this queue. + * + * If the queue has reached its maximum capacity, the first (oldest) element is evicted (removed) + * before the new element is added. + * + * This queue should never throw [IllegalStateException] due to capacity restriction of the [delegate] because it + * uses [java.util.Queue.offer] to insert elements. + * + * @param element the element to be added. + * + * @return `true` if this collection changed as a result of the call (as specified by [java.util.Collection.add]) + */ + override fun add(element: T): Boolean { + return this.offer(element) + } + + /** + * Adds the specified [element] to the end of this queue. + * + * If the queue has reached its maximum capacity, the first (oldest) element is evicted (removed) + * before the new element is added. + * + * @param element the element to be added. + * + * @return `true` if this collection changed as a result of the call + */ + override fun offer(element: T): Boolean { + if (maxSize == 0) return false + if (size >= maxSize) { + delegate.poll() + } + + @Suppress("UnsafeThirdPartyFunctionCall") // can't have NPE here + return delegate.offer(element) + } + + /** + * Adds all of the elements in the specified [elements] collection to the end of this queue. + * + * If the number of elements in [elements] is greater than or equal to [maxSize], the queue is cleared first, + * and only the last [maxSize] elements from [elements] are added. + * + * Otherwise, if adding [elements] would exceed the maximum capacity, the required number of oldest elements + * are evicted from the front of the queue to make room. + * + * @param elements the collection of elements to be added. + * @return `true` if the queue changed as a result of the call. + */ + override fun addAll(elements: Collection): Boolean { + return when { + maxSize == 0 -> false + + elements.size >= maxSize -> { + clear() + for ((index, element) in elements.withIndex()) { + if (index < elements.size - maxSize) continue + delegate.add(element) + } + true + } + + else -> { + val spaceLeft = maxSize - size + for (index in 0 until elements.size - spaceLeft) { + delegate.poll() + } + + delegate.addAll(elements) + } + } + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/network/GraphQLHeaders.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/network/GraphQLHeaders.kt new file mode 100644 index 0000000000..cf6ec9ba05 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/network/GraphQLHeaders.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.network + +/** + * Headers used internally for intercepting GraphQL requests. + * @param headerValue name of the header. + */ +enum class GraphQLHeaders(val headerValue: String) { + DD_GRAPHQL_NAME_HEADER("_dd-custom-header-graph-ql-operation-name"), + DD_GRAPHQL_VARIABLES_HEADER("_dd-custom-header-graph-ql-variables"), + DD_GRAPHQL_TYPE_HEADER("_dd-custom-header-graph-ql-operation_type"), + DD_GRAPHQL_PAYLOAD_HEADER("_dd-custom-header-graph-ql-payload") +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkCounter.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkCounter.kt new file mode 100644 index 0000000000..396c571f24 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkCounter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Interface of benchmark counter to be implemented. This should only be used by internal benchmarking. + */ +@NoOpImplementation +interface BenchmarkCounter { + + /** + * Adds a value to the benchmark counter. + * @param value The value to be added. + * @param attributes The attributes to be associated with the value. + */ + fun add( + value: Long, + attributes: Map + ) +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkMeter.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkMeter.kt new file mode 100644 index 0000000000..677ff7cfc3 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkMeter.kt @@ -0,0 +1,35 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Provides an interface to collect counters and gauges in the benchmark environment. + * During benchmarking a concrete implementation is used, but in production it is a noop + */ +@NoOpImplementation +interface BenchmarkMeter { + + /** + * Gets a [BenchmarkCounter] for the given counter name. + * + */ + fun getCounter( + operation: String + ): BenchmarkCounter + + /** + * Creates an observable gauge for the parameters. + * + */ + fun createObservableGauge( + metricName: String, + tags: Map, + callback: () -> Double + ) +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkProfiler.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkProfiler.kt new file mode 100644 index 0000000000..c3c08863ec --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkProfiler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Interface of benchmark profiler to be implemented to provide [BenchmarkTracer]. + * This should only used by internal benchmarking. + */ +@NoOpImplementation +interface BenchmarkProfiler { + + /** + * Returns a [BenchmarkTracer]. + */ + fun getTracer(operation: String): BenchmarkTracer +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSdkUploads.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSdkUploads.kt new file mode 100644 index 0000000000..7db5481ef8 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSdkUploads.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Interface of benchmark SDK performance to be implemented. This should only be used by internal + * benchmarking. + */ +@NoOpImplementation +interface BenchmarkSdkUploads { + + /** + * Get a [BenchmarkMeter] for the given operation. + * @param operation The operation name. + * @return The [BenchmarkMeter] for the given operation. + */ + fun getMeter(operation: String): BenchmarkMeter +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpan.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpan.kt new file mode 100644 index 0000000000..8965bccf95 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpan.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Interface of benchmark span to be implemented. This should only used by internal benchmarking. + */ +@NoOpImplementation +interface BenchmarkSpan { + + /** + * Marks the end of [BenchmarkSpan] execution. + */ + fun stop() +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpanBuilder.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpanBuilder.kt new file mode 100644 index 0000000000..cff64bf992 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpanBuilder.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Interface of benchmark span builder. This should only used by internal benchmarking. + */ +@NoOpImplementation +interface BenchmarkSpanBuilder { + + /** + * Returns a new [BenchmarkSpan] and start the span. + */ + fun startSpan(): BenchmarkSpan +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpanExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpanExt.kt new file mode 100644 index 0000000000..20567dfcba --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkSpanExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +/** + * Wraps the provided lambda within a [BenchmarkSpan]. + * @param T the type returned by the lambda + * @param operationName the name of the [BenchmarkSpan] created around the lambda + * (default is `true`) + * @param additionalProperties Additional properties for this span. + * @param block the lambda function traced by this newly created [BenchmarkSpan] + * + */ +inline fun withinBenchmarkSpan( + operationName: String, + additionalProperties: Map = emptyMap(), + block: BenchmarkSpan.() -> T +): T { + val tracer = GlobalBenchmark.getProfiler().getTracer("dd-sdk-android") + + val spanBuilder = tracer.spanBuilder( + operationName, + additionalProperties + ) + + val span = spanBuilder.startSpan() + + return try { + span.block() + } finally { + span.stop() + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkTracer.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkTracer.kt new file mode 100644 index 0000000000..b940819d62 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/BenchmarkTracer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Interface of benchmark tracer to be implemented to provide [BenchmarkSpan]. + * This should only used by internal benchmarking. + */ +@NoOpImplementation +interface BenchmarkTracer { + + /** + * Returns a new [BenchmarkSpanBuilder]. + * + * @param spanName The name of the returned span. + * @param additionalProperties Additional properties for this span. + * @return a new [BenchmarkSpanBuilder]. + */ + fun spanBuilder( + spanName: String, + additionalProperties: Map = emptyMap() + ): BenchmarkSpanBuilder +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/DDExecutionTimer.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/DDExecutionTimer.kt new file mode 100644 index 0000000000..a8f07bf9a2 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/DDExecutionTimer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +internal class DDExecutionTimer( + private val track: String, + private val benchmarkSdkUploads: BenchmarkSdkUploads = GlobalBenchmark.getBenchmarkSdkUploads() +) : ExecutionTimer { + override fun measure(action: () -> T): T { + if (track.isEmpty()) { + return action() + } + + val requestStartTime = System.nanoTime() + val result = action() + val latencyInSeconds = (System.nanoTime() - requestStartTime) / NANOSECONDS_IN_A_SECOND + responseLatencyReport(latencyInSeconds, track) + return result + } + + private fun responseLatencyReport(latencySeconds: Double, track: String) { + val tags = mapOf( + TRACK_NAME to track + ) + + benchmarkSdkUploads + .getMeter(METER_NAME) + .createObservableGauge(BENCHMARK_RESPONSE_LATENCY, tags) { + latencySeconds + } + } + + private companion object { + private const val TRACK_NAME = "track" + private const val METER_NAME = "dd-sdk-android" + private const val BENCHMARK_RESPONSE_LATENCY = "android.benchmark.response_latency" + private const val NANOSECONDS_IN_A_SECOND = 1_000_000_000.0 + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/ExecutionTimer.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/ExecutionTimer.kt new file mode 100644 index 0000000000..e458a64a23 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/ExecutionTimer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +/** + * Interface for execution timers to measure the duration of actions. This should only be used by internal + * benchmarking. + */ +interface ExecutionTimer { + + /** + * Wraps the action to measure the time it took to execute. + * @param T The type of the result. + * @param action The action to measure. + */ + fun measure(action: () -> T): T +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/GlobalBenchmark.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/GlobalBenchmark.kt new file mode 100644 index 0000000000..f80a5f7d45 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/GlobalBenchmark.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +/** + * A global holder of [BenchmarkProfiler] + * allowing registration and retrieval of [BenchmarkProfiler] and [BenchmarkMeter] implementations. + * This should only used by internal benchmarking. + */ +object GlobalBenchmark { + + private var benchmarkProfiler: BenchmarkProfiler = NoOpBenchmarkProfiler() + private var benchmarkSdkUploads: BenchmarkSdkUploads = + NoOpBenchmarkSdkUploads() + + /** + * Registers the implementation of [BenchmarkProfiler]. + */ + fun register(benchmarkProfiler: BenchmarkProfiler) { + this.benchmarkProfiler = benchmarkProfiler + } + + /** + * Registers the implementation of [BenchmarkSdkUploads]. + */ + fun register(benchmarkSdkUploads: BenchmarkSdkUploads) { + this.benchmarkSdkUploads = benchmarkSdkUploads + } + + /** + * Returns the [BenchmarkProfiler] registered. + */ + fun getProfiler(): BenchmarkProfiler { + return benchmarkProfiler + } + + /** + * Returns the [BenchmarkSdkUploads] registered. + */ + fun getBenchmarkSdkUploads(): BenchmarkSdkUploads { + return benchmarkSdkUploads + } + + /** + * Creates the appropriate [ExecutionTimer]. + */ + fun createExecutionTimer(track: String): ExecutionTimer { + if (benchmarkSdkUploads is NoOpBenchmarkSdkUploads) { + return NoOpExecutionTimer() + } + + return DDExecutionTimer( + track = track + ) + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/NoOpExecutionTimer.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/NoOpExecutionTimer.kt new file mode 100644 index 0000000000..82b73e5936 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/profiler/NoOpExecutionTimer.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.profiler + +internal class NoOpExecutionTimer : ExecutionTimer { + override fun measure(action: () -> T): T { + return action() + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/InternalTelemetryEvent.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/InternalTelemetryEvent.kt new file mode 100644 index 0000000000..d036572c03 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/InternalTelemetryEvent.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.telemetry + +import com.datadog.android.internal.utils.loggableStackTrace + +@Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty") +sealed class InternalTelemetryEvent { + + sealed class Log( + val message: String, + val additionalProperties: Map? + ) : InternalTelemetryEvent() { + class Debug(message: String, additionalProperties: Map?) : Log(message, additionalProperties) + + class Error( + message: String, + additionalProperties: Map? = null, + val error: Throwable? = null, + val stacktrace: String? = null, + val kind: String? = null + ) : Log(message, additionalProperties) { + fun resolveKind(): String? { + return kind ?: error?.javaClass?.canonicalName ?: error?.javaClass?.simpleName + } + + fun resolveStacktrace(): String? { + return stacktrace ?: error?.loggableStackTrace() + } + } + } + + data class Configuration( + val trackErrors: Boolean, + val batchSize: Long, + val batchUploadFrequency: Long, + val useProxy: Boolean, + val useLocalEncryption: Boolean, + val batchProcessingLevel: Int + ) : InternalTelemetryEvent() + + data class Metric( + val message: String, + val additionalProperties: Map? + ) : InternalTelemetryEvent() + + sealed class ApiUsage(val additionalProperties: MutableMap = mutableMapOf()) : + InternalTelemetryEvent() { + class AddViewLoadingTime( + val overwrite: Boolean, + val noView: Boolean, + val noActiveView: Boolean, + additionalProperties: MutableMap = mutableMapOf() + ) : ApiUsage(additionalProperties) + + class AddOperationStepVital( + val actionType: ActionType, + additionalProperties: MutableMap = mutableMapOf() + ) : ApiUsage(additionalProperties) { + enum class ActionType { + START, + SUCCEED, + FAIL + } + } + } + + object InterceptorInstantiated : InternalTelemetryEvent() +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/TracingHeaderType.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/TracingHeaderType.kt new file mode 100644 index 0000000000..408cf0da6b --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/TracingHeaderType.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.telemetry + +@Suppress("UndocumentedPublicClass") +enum class TracingHeaderType { + /** + * 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). + */ + DATADOG, + + /** + * Open Telemetry B3 [Single header](https://github.com/openzipkin/b3-propagation#single-header). + */ + B3, + + /** + * Open Telemetry B3 [Multiple headers](https://github.com/openzipkin/b3-propagation#multiple-headers). + */ + B3MULTI, + + /** + * W3C [Trace Context header](https://www.w3.org/TR/trace-context/#tracestate-header). + */ + TRACECONTEXT +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/TracingHeaderTypesSet.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/TracingHeaderTypesSet.kt new file mode 100644 index 0000000000..e57dd98609 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/telemetry/TracingHeaderTypesSet.kt @@ -0,0 +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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.telemetry + +@Suppress("UndocumentedPublicClass", "UndocumentedPublicProperty") +data class TracingHeaderTypesSet( + val types: Set +) diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/thread/NamedExecutionUnit.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/thread/NamedExecutionUnit.kt new file mode 100644 index 0000000000..7704a02243 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/thread/NamedExecutionUnit.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.thread + +import java.util.Locale +import java.util.concurrent.Callable + +/** + * Provides a possibility to attach human-readable names to the execution units. + */ +interface NamedExecutionUnit { + /** + * Sanitized name after replacing spaces, colons, periods, and commas + * with underscores (`_`), and converting all characters to lowercase. + */ + val name: String +} + +private val SanitizedRegex = "[ :.,]".toRegex() + +/** + * A wrapper around a [Runnable] that assigns it a sanitized, lowercase name. + * + * This class is useful when you want to associate a human-readable name with a [Runnable], + * for logging, debugging, or tracking purposes. + * + * The provided [name] is sanitized by replacing spaces, colons, periods, and commas + * with underscores (`_`), and converting all characters to lowercase. + * + * @param name The name to associate with this runnable, will be sanitized for safe usage. + * @param runnable The actual runnable to be executed when [run] is called. + */ +class NamedRunnable(name: String, private val runnable: Runnable) : NamedExecutionUnit, Runnable by runnable { + + /** + * Sanitized name after replacing spaces, colons, periods, and commas + * with underscores (`_`), and converting all characters to lowercase. + */ + override val name: String = name.replace(SanitizedRegex, "_").lowercase(Locale.US) +} + +/** + * A wrapper around a [Callable] that assigns it a sanitized, lowercase name. + * + * This class is useful when you want to associate a human-readable name with a [Callable], + * for logging, debugging, or tracking purposes. + * + * The provided [name] is sanitized by replacing spaces, colons, periods, and commas + * with underscores (`_`), and converting all characters to lowercase. + * + * @param V The type of the value returned by the callable. + * @param name The name to associate with this callable, will be sanitized for safe usage. + * @param callable The actual callable to be executed when [call] is called. + */ +class NamedCallable(name: String, private val callable: Callable) : NamedExecutionUnit, Callable by callable { + + /** + * Sanitized name after replacing spaces, colons, periods, and commas + * with underscores (`_`), and converting all characters to lowercase. + */ + override val name: String = name.replace(SanitizedRegex, "_").lowercase(Locale.US) +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/DefaultTimeProvider.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/DefaultTimeProvider.kt new file mode 100644 index 0000000000..b60ff61293 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/DefaultTimeProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.time + +/** + * A [TimeProvider] implementation that provides the current device time as both device and server time. + * The offsets are always 0. + */ +class DefaultTimeProvider : TimeProvider { + override fun getDeviceTimestamp(): Long = System.currentTimeMillis() + + override fun getServerTimestamp(): Long = System.currentTimeMillis() + + override fun getServerOffsetNanos(): Long = 0L + + override fun getServerOffsetMillis(): Long = 0L +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/TimeProvider.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/TimeProvider.kt new file mode 100644 index 0000000000..030f550d52 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/TimeProvider.kt @@ -0,0 +1,35 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.time + +// there is no NoOpImplementation on purpose, we don't want to have 0 values for the +// case when this instance is used. +/** + * Interface to provide the current time in both device and server time references. + */ +interface TimeProvider { + + /** + * Returns the current device timestamp in milliseconds. + */ + fun getDeviceTimestamp(): Long + + /** + * Returns the current server timestamp in milliseconds. + */ + fun getServerTimestamp(): Long + + /** + * Returns the offset between the device and server time references in nanoseconds. + */ + fun getServerOffsetNanos(): Long + + /** + * Returns the offset between the device and server time references in milliseconds. + */ + fun getServerOffsetMillis(): Long +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ByteArrayExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ByteArrayExt.kt new file mode 100644 index 0000000000..204f60bf2e --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ByteArrayExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +private const val BYTE_MASK = 0xff +private const val HEX_SHIFT = 4 +private const val LOWER_NIBBLE_MASK = 0x0f +private const val HEX_CHARS = "0123456789abcdef" + +/** + * Converts a ByteArray to its corresponding hexadecimal String representation. + * + * Each byte in the array is converted into two hexadecimal characters. + * For example, the byte array `[0xA, 0x1F]` will be converted to the string `"0a1f"`. + * + * This method avoids performance overhead by using bitwise operations and + * minimizing object allocations compared to alternatives like `joinToString`. + * + * @receiver ByteArray The byte array to be converted. + * @return A hexadecimal [String] representation of the byte array. + * + */ +fun ByteArray.toHexString(): String { + @Suppress("UnsafeThirdPartyFunctionCall") // byte array size is always positive. + val result = StringBuilder(size * 2) + for (byte in this) { + val intVal = byte.toInt() and BYTE_MASK + result.append(HEX_CHARS[intVal ushr HEX_SHIFT]) // Append first half of byte + result.append(HEX_CHARS[intVal and LOWER_NIBBLE_MASK]) // Append second half of byte + } + return result.toString() +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/DDCoreSubscription.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/DDCoreSubscription.kt new file mode 100644 index 0000000000..e21cbe40d3 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/DDCoreSubscription.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import java.util.concurrent.CopyOnWriteArraySet + +/** + * A utility that holds listeners and notifies them. It satisfies the following requirements: + * 1. All methods can be called from any thread. + * 2. It is possible to call [addListener] and [removeListener] inside a listener callback. + * 3. Listeners are notified in the order [addListener] is called on them. + */ +@Suppress("UndocumentedPublicFunction", "UndocumentedPublicProperty") +interface DDCoreSubscription { + fun addListener(listener: T) + + fun removeListener(listener: T) + + fun notifyListeners(block: T.() -> Unit) + + val listenersCount: Int + + companion object { + fun create(): DDCoreSubscription { + return DDCoreSubscriptionImpl() + } + } +} + +private class DDCoreSubscriptionImpl : DDCoreSubscription { + private val listeners = CopyOnWriteArraySet() + + override fun addListener(listener: T) { + listeners.add(listener) + } + + override fun removeListener(listener: T) { + listeners.remove(listener) + } + + override fun notifyListeners(block: T.() -> Unit) { + listeners.forEach { it.block() } + } + + override val listenersCount: Int get() = listeners.size +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ImageViewUtils.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ImageViewUtils.kt new file mode 100644 index 0000000000..25933aff16 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ImageViewUtils.kt @@ -0,0 +1,248 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.ImageView + +/** + * A collection of view utility functions for resolving absolute + * positions, clipping bounds, and other useful data for + * image views operations. + */ +object ImageViewUtils { + /** + * Resolves the absolute position on the screen of the given [View]. + * @param view the [View]. + * @param cropToPadding if the view has cropToPadding as true. + * @return the [Rect] representing the absolute position of the view. + */ + fun resolveParentRectAbsPosition(view: View, cropToPadding: Boolean = true): Rect { + val coords = IntArray(2) + // this will always have size >= 2 + @Suppress("UnsafeThirdPartyFunctionCall") + view.getLocationOnScreen(coords) + val leftPadding = if (cropToPadding) view.paddingLeft else 0 + val rightPadding = if (cropToPadding) view.paddingRight else 0 + val topPadding = if (cropToPadding) view.paddingTop else 0 + val bottomPadding = if (cropToPadding) view.paddingBottom else 0 + return Rect( + coords[0] + leftPadding, + coords[1] + topPadding, + coords[0] + view.width - rightPadding, + coords[1] + view.height - bottomPadding + ) + } + + /** + * Calculates the clipping [Rect] of the given child [Rect] using its parent [Rect] and + * the screen density. + * @param parentRect the parent [Rect]. + * @param childRect the child [Rect]. + * @param density the screen density. + * @return the clipping [Rect]. + */ + fun calculateClipping(parentRect: Rect, childRect: Rect, density: Float): Rect { + val left = if (childRect.left < parentRect.left) { + parentRect.left - childRect.left + } else { + 0 + } + val top = if (childRect.top < parentRect.top) { + parentRect.top - childRect.top + } else { + 0 + } + val right = if (childRect.right > parentRect.right) { + childRect.right - parentRect.right + } else { + 0 + } + val bottom = if (childRect.bottom > parentRect.bottom) { + childRect.bottom - parentRect.bottom + } else { + 0 + } + return Rect( + left.densityNormalized(density), + top.densityNormalized(density), + right.densityNormalized(density), + bottom.densityNormalized(density) + ) + } + + /** + * Resolves the [Drawable] content [Rect] using the given [ImageView] scale type. + * @param imageView the [ImageView]. + * @param drawable the [Drawable]. + * @param customScaleType optional custom [ImageView.ScaleType]. + * @return the resolved content [Rect]. + */ + fun resolveContentRectWithScaling( + imageView: ImageView, + drawable: Drawable, + customScaleType: ImageView.ScaleType? = null + ): Rect { + val drawableWidthPx = drawable.intrinsicWidth + val drawableHeightPx = drawable.intrinsicHeight + + val parentRect = resolveParentRectAbsPosition(imageView) + + val childRect = Rect( + 0, + 0, + drawableWidthPx, + drawableHeightPx + ) + + val resultRect: Rect + + when (customScaleType ?: imageView.scaleType) { + ImageView.ScaleType.FIT_START -> { + val contentRect = scaleRectToFitParent(parentRect, childRect) + resultRect = positionRectAtStart(parentRect, contentRect) + } + ImageView.ScaleType.FIT_END -> { + val contentRect = scaleRectToFitParent(parentRect, childRect) + resultRect = positionRectAtEnd(parentRect, contentRect) + } + ImageView.ScaleType.FIT_CENTER -> { + val contentRect = scaleRectToFitParent(parentRect, childRect) + resultRect = positionRectInCenter(parentRect, contentRect) + } + ImageView.ScaleType.CENTER_INSIDE -> { + val contentRect = scaleRectToCenterInsideParent(parentRect, childRect) + resultRect = positionRectInCenter(parentRect, contentRect) + } + ImageView.ScaleType.CENTER -> { + resultRect = positionRectInCenter(parentRect, childRect) + } + ImageView.ScaleType.CENTER_CROP -> { + val contentRect = scaleRectToCenterCrop(parentRect, childRect) + resultRect = positionRectInCenter(parentRect, contentRect) + } + ImageView.ScaleType.FIT_XY, + ImageView.ScaleType.MATRIX, + null -> { + resultRect = Rect( + parentRect.left, + parentRect.top, + parentRect.right, + parentRect.bottom + ) + } + } + + return resultRect + } + + private fun scaleRectToCenterInsideParent( + parentRect: Rect, + childRect: Rect + ): Rect { + // it already fits inside the parent + if (parentRect.width() > childRect.width() && parentRect.height() > childRect.height()) { + return childRect + } + + val scaleX: Float = parentRect.width().toFloat() / childRect.width().toFloat() + val scaleY: Float = parentRect.height().toFloat() / childRect.height().toFloat() + + var scaleFactor: Float = minOf(scaleX, scaleY) + + // center inside doesn't enlarge, it only reduces + if (scaleFactor >= 1F) scaleFactor = 1F + + val newWidth = childRect.width() * scaleFactor + val newHeight = childRect.height() * scaleFactor + + val resultRect = Rect() + resultRect.left = parentRect.left + resultRect.top = parentRect.top + resultRect.right = resultRect.left + newWidth.toInt() + resultRect.bottom = resultRect.top + newHeight.toInt() + return resultRect + } + + private fun scaleRectToCenterCrop( + parentRect: Rect, + childRect: Rect + ): Rect { + val scaleX: Float = parentRect.width().toFloat() / childRect.width().toFloat() + val scaleY: Float = parentRect.height().toFloat() / childRect.height().toFloat() + val scaleFactor = maxOf(scaleX, scaleY) + + val newWidth = childRect.width() * scaleFactor + val newHeight = childRect.height() * scaleFactor + + val resultRect = Rect() + resultRect.left = 0 + resultRect.top = 0 + resultRect.right = newWidth.toInt() + resultRect.bottom = newHeight.toInt() + return resultRect + } + + private fun scaleRectToFitParent( + parentRect: Rect, + childRect: Rect + ): Rect { + val scaleX: Float = parentRect.width().toFloat() / childRect.width().toFloat() + val scaleY: Float = parentRect.height().toFloat() / childRect.height().toFloat() + val scaleFactor = minOf(scaleX, scaleY) + + val newWidth = childRect.width() * scaleFactor + val newHeight = childRect.height() * scaleFactor + + val resultRect = Rect() + resultRect.left = 0 + resultRect.top = 0 + resultRect.right = newWidth.toInt() + resultRect.bottom = newHeight.toInt() + return resultRect + } + + private fun positionRectInCenter(parentRect: Rect, childRect: Rect): Rect { + val centerXParentPx = parentRect.centerX() + val centerYParentPx = parentRect.centerY() + val childRectWidthPx = childRect.width() + val childRectHeightPx = childRect.height() + + val resultRect = Rect() + resultRect.left = centerXParentPx - (childRectWidthPx / 2) + resultRect.top = centerYParentPx - (childRectHeightPx / 2) + resultRect.right = resultRect.left + childRectWidthPx + resultRect.bottom = resultRect.top + childRectHeightPx + return resultRect + } + + private fun positionRectAtStart(parentRect: Rect, childRect: Rect): Rect { + val childRectWidthPx = childRect.width() + val childRectHeightPx = childRect.height() + + val resultRect = Rect() + resultRect.left = parentRect.left + resultRect.top = parentRect.top + resultRect.right = resultRect.left + childRectWidthPx + resultRect.bottom = resultRect.top + childRectHeightPx + return resultRect + } + + private fun positionRectAtEnd(parentRect: Rect, childRect: Rect): Rect { + val childRectWidthPx = childRect.width() + val childRectHeightPx = childRect.height() + + val resultRect = Rect() + resultRect.right = parentRect.right + resultRect.bottom = parentRect.bottom + resultRect.left = parentRect.right - childRectWidthPx + resultRect.top = parentRect.bottom - childRectHeightPx + return resultRect + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/IntExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/IntExt.kt new file mode 100644 index 0000000000..04d5528f34 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/IntExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +/** + * Normalizes an Int value (font size, view dimension, view position, etc.) according with the + * device pixels density. + * Example: if a device has a DPI = 2, the normalized height of a view will be + * view.height/2. + * @param density + */ +fun Int.densityNormalized(density: Float): Int { + if (density == 0f) { + return this + } + return (this / density).toInt() +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/LongExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/LongExt.kt new file mode 100644 index 0000000000..b057e92220 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/LongExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +/** + * Normalizes a Long value (font size, view dimension, view position, etc.) according with the + * device pixels density. + * Example: if a device has a DPI = 2, the normalized height of a view will be + * view.height/2. + * @param density + */ +fun Long.densityNormalized(density: Float): Long { + if (density == 0f) { + return this + } + return (this / density).toLong() +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/MapUtils.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/MapUtils.kt new file mode 100644 index 0000000000..cf99fd25fa --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/MapUtils.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +// TODO RUM-373 public as hack, no other solution for now. Any?.toJsonElement relies on this +// particular value. Maybe create something like (class NullMap) and check identity instead? +/** + * Special value for missing attribute. + */ +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "PackageNameVisibility") +val NULL_MAP_VALUE: Object = Object() diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/NumberExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/NumberExt.kt new file mode 100644 index 0000000000..dfd6feec39 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/NumberExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import java.math.BigInteger + +/** + * Radix used to convert numbers to hexadecimal strings. + */ +private const val HEX_RADIX: Int = 16 + +/** + * Converts this [Int] into hexadecimal representation. + */ +fun Int.toHexString(): String = toString(HEX_RADIX) + +/** + * Converts this [Long] into hexadecimal representation. + */ +fun Long.toHexString(): String = toString(HEX_RADIX) + +/** + * Converts this [BigInteger] into hexadecimal representation. + */ +fun BigInteger.toHexString(): String { + return toLong().toHexString() +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/StrictModeExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/StrictModeExt.kt new file mode 100644 index 0000000000..eae1eca9a6 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/StrictModeExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import android.os.StrictMode + +/** + * This utility function wraps a call to a method that needs to perform a disk read operation + * on the main thread. + * This prevents adding LogCat noise when customer enable StrictMode logging. + * @param T the type returned by the operation + * @param operation the operation + * @return the value returned by the operation + */ +fun allowThreadDiskReads( + operation: () -> T +): T { + val oldPolicy = StrictMode.allowThreadDiskReads() + try { + return operation() + } finally { + StrictMode.setThreadPolicy(oldPolicy) + } +} + +/** + * This utility function wraps a call to a method that needs to perform a disk write operation + * on the main thread. + * This prevents adding LogCat noise when customer enable StrictMode logging. + * @param T the type returned by the operation + * @param operation the operation + * @return the value returned by the operation + */ +fun allowThreadDiskWrites( + operation: () -> T +): T { + val oldPolicy = StrictMode.allowThreadDiskWrites() + try { + return operation() + } finally { + StrictMode.setThreadPolicy(oldPolicy) + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/StringBuilderExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/StringBuilderExt.kt new file mode 100644 index 0000000000..1c57cbe6b9 --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/StringBuilderExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +/** + * This utility function helps to replace [joinToString] calls with more efficient [StringBuilder] methods calls. + * In case when content should be separated with some separator, it's handy to add it in front of a new string only + * if buffer already contains some data. + * @param str string that should be added to the buffer only if it already contains some data. + */ +fun StringBuilder.appendIfNotEmpty(str: String) = apply { + if (isNotEmpty()) append(str) +} + +/** + * This utility function helps to replace [joinToString] calls with more efficient [StringBuilder] methods calls. + * In case when content should be separated with some separator, it's handy to add it in front of a new string only + * if buffer already contains some data. + * @param char char that should be added to the buffer only if it already contains some data. + */ +fun StringBuilder.appendIfNotEmpty(char: Char) = apply { + if (isNotEmpty()) append(char) +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ThreadExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ThreadExt.kt new file mode 100644 index 0000000000..e88a78d38d --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ThreadExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import android.os.Build + +/** + * getId() method got deprecated on Android 36. + * https://android-review.googlesource.com/c/platform/libcore/+/3380110/3/ojluni/src/main/java/java/lang/Thread.java#b2114 + * But threadId() is part of hidden API before Android 36, so we use getId() on those older versions. + */ +fun Thread.safeGetThreadId(): Long { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + threadId() + } else { + @Suppress("DEPRECATION") + id + } +} + +/** + * Converts Thread state to string format. This is needed, because enum may be obfuscated, so we + * cannot rely on the name property. + */ +fun Thread.State.asString(): String { + return when (this) { + Thread.State.NEW -> "new" + Thread.State.BLOCKED -> "blocked" + Thread.State.RUNNABLE -> "runnable" + Thread.State.TERMINATED -> "terminated" + Thread.State.TIMED_WAITING -> "timed_waiting" + Thread.State.WAITING -> "waiting" + } +} + +/** + * Converts stacktrace to string format. + */ +fun Array.loggableStackTrace(): String = joinToString("\n") { "at $it" } diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ThrowableExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ThrowableExt.kt new file mode 100644 index 0000000000..78e28b213d --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ThrowableExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import java.io.PrintWriter +import java.io.StringWriter + +/** + * Converts stacktrace to string format. + */ +fun Throwable.loggableStackTrace(): String { + val stringWriter = StringWriter() + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + printStackTrace(PrintWriter(stringWriter)) + return stringWriter.toString() +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/rum/DdRumContentProvider.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/rum/DdRumContentProvider.kt new file mode 100644 index 0000000000..a26fc234ad --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/rum/DdRumContentProvider.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum + +import android.app.ActivityManager +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Process +import android.util.Log + +/** + * A Content provider used to monitor the Application startup time efficiently. + */ +class DdRumContentProvider : ContentProvider() { + + override fun onCreate(): Boolean { + if (processImportance == 0) { + val manager = context?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + val currentProcessId = Process.myPid() + val currentProcess = manager?.runningAppProcesses?.firstOrNull { + it.pid == currentProcessId + } + processImportance = currentProcess?.importance ?: DEFAULT_IMPORTANCE + Log.w("DdRumContentProvider", "processImportance:$processImportance") + } + return true + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + return null + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + return 0 + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + return 0 + } + + companion object { + internal const val DEFAULT_IMPORTANCE: Int = + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + + /** + * Process importance at the moment of creating [DdRumContentProvider] + * Should be set from the outside only in tests. + */ + var processImportance: Int = 0 + + /** + * fallback for APIs below Android N, see [DefaultAppStartTimeProvider]. + */ + val createTimeNs: Long = System.nanoTime() + } +} diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/tools/annotation/NoOpImplementation.kt b/dd-sdk-android-internal/src/main/java/com/datadog/tools/annotation/NoOpImplementation.kt new file mode 100644 index 0000000000..79cf2db62a --- /dev/null +++ b/dd-sdk-android-internal/src/main/java/com/datadog/tools/annotation/NoOpImplementation.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.annotation + +/** + * Adding this annotation on an interface will generate a No-Op implementation class. + * @property publicNoOpImplementation if true, the NoOp implementation will be made public, + * otherwise it will be marked as Internal (default: false) + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class NoOpImplementation( + val publicNoOpImplementation: Boolean = false +) diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/collections/EvictingQueueTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/collections/EvictingQueueTest.kt new file mode 100644 index 0000000000..64c73243d7 --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/collections/EvictingQueueTest.kt @@ -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 2016-Present Datadog, Inc. + */ +package com.datadog.android.internal.collections + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +internal class EvictingQueueTest { + + @Test + fun `M shrink items W add {more than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + repeat(5) { queue.add(it) } + + // Then + assertThat(queue.toList()).isEqualTo(listOf(2, 3, 4)) + } + + @Test + fun `M shrink items W offer {more than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + repeat(5) { queue.offer(it) } + + // Then + assertThat(queue.toList()).isEqualTo(listOf(2, 3, 4)) + } + + @Test + fun `M not shrink items W add {less than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + repeat(2) { queue.add(it) } + + // Then + assertThat(queue.toList()).isEqualTo(listOf(0, 1)) + } + + @Test + fun `M not shrink items W offer {less than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + repeat(2) { queue.offer(it) } + + // Then + assertThat(queue.toList()).isEqualTo(listOf(0, 1)) + } + + @Test + fun `M not shrink items W addAll {less than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + queue.addAll(listOf(1, 2)) + + // Then + assertThat(queue.toList()).isEqualTo(listOf(1, 2)) + } + + @Test + fun `M not shrink items W addAll {equal max}`() { + // Given + val queue = EvictingQueue(3) + + // When + queue.addAll(listOf(1, 2)) + + // Then + assertThat(queue.toList()).isEqualTo(listOf(1, 2)) + } + + @Test + fun `M shrink items W addAll {more than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + queue.addAll(listOf(1, 2, 3, 4)) + + // Then + assertThat(queue.toList()).isEqualTo(listOf(2, 3, 4)) + } + + @Test + fun `M shrink items W addAll {more than max, less than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + queue.addAll(listOf(1, 2, 3, 4)) + queue.addAll(listOf(5, 6)) + + // Then + assertThat(queue.toList()).isEqualTo(listOf(4, 5, 6)) + } + + @Test + fun `M shrink items W addAll {more than max, more than max}`() { + // Given + val queue = EvictingQueue(3) + + // When + queue.addAll(listOf(1, 2, 3, 4)) + queue.addAll(listOf(5, 6, 7, 8)) + + // Then + assertThat(queue.toList()).isEqualTo(listOf(6, 7, 8)) + } + + @Test + fun `M shrink items W addAll {equal max, equal max}`() { + // Given + val queue = EvictingQueue(3) + + // When + queue.addAll(listOf(1, 2, 3)) + queue.addAll(listOf(4, 5, 6)) + + // Then + assertThat(queue.toList()).isEqualTo(listOf(4, 5, 6)) + } + + @Test + fun `M create empty queue W maxSize le 0`() { + // When + val queue = EvictingQueue(-1) + + // Then + assertThat(queue.size).isEqualTo(0) + } + + @Test + fun `M not change 0-sized queue W add`() { + // Given + val queue = EvictingQueue(0) + + // When + assertDoesNotThrow { queue.add(1) } + + // Then + assertThat(queue.size).isEqualTo(0) + } + + @Test + fun `M not change 0-sized queue W offer`() { + // Given + val queue = EvictingQueue(0) + + // When + assertDoesNotThrow { queue.offer(1) } + + // Then + assertThat(queue.size).isEqualTo(0) + } +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/forge/Configurator.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/forge/Configurator.kt new file mode 100644 index 0000000000..ac3a5aa243 --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/forge/Configurator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.forge + +import com.datadog.android.internal.tests.elmyr.InternalTelemetryErrorLogForgeryFactory +import com.datadog.tools.unit.forge.BaseConfigurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.jvm.useJvmFactories + +internal class Configurator : + BaseConfigurator() { + override fun configure(forge: Forge) { + super.configure(forge) + forge.useJvmFactories() + forge.addFactory(InternalTelemetryErrorLogForgeryFactory()) + } +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/telemetry/InternalTelemetryErrorEventTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/telemetry/InternalTelemetryErrorEventTest.kt new file mode 100644 index 0000000000..6122cb31e4 --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/telemetry/InternalTelemetryErrorEventTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.telemetry + +import com.datadog.android.internal.utils.loggableStackTrace +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class InternalTelemetryErrorEventTest { + + @Test + fun `M resolve the given stacktrace W resolveStacktrace { stacktrace explicitly provided }`(forge: Forge) { + // Given + val expectedStackTrace = forge.aString() + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = null, + stacktrace = expectedStackTrace, + kind = forge.aNullable { aString() } + ) + + // When + val resolvedStackTrace = errorEvent.resolveStacktrace() + assertThat(resolvedStackTrace).isEqualTo(expectedStackTrace) + } + + @Test + fun `M resolve the given stacktrace W resolveStacktrace { stacktrace and throwable explicitly provided }`( + forge: Forge + ) { + // Given + val expectedStackTrace = forge.aString() + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = forge.aThrowable(), + stacktrace = expectedStackTrace, + kind = forge.aNullable { aString() } + ) + + // When + val resolvedStackTrace = errorEvent.resolveStacktrace() + assertThat(resolvedStackTrace).isEqualTo(expectedStackTrace) + } + + @Test + fun `M resolve throwable stacktrace W resolveStacktrace { only throwable explicitly provided }`( + forge: Forge + ) { + // Given + val fakeThrowable = forge.aThrowable() + val expectedStackTrace = fakeThrowable.loggableStackTrace() + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = fakeThrowable, + stacktrace = null, + kind = forge.aNullable { aString() } + ) + + // When + val resolvedStackTrace = errorEvent.resolveStacktrace() + assertThat(resolvedStackTrace).isEqualTo(expectedStackTrace) + } + + @Test + fun `M resolve null W resolveStacktrace { stacktrace nor throwable provided }`( + forge: Forge + ) { + // Given + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = null, + stacktrace = null, + kind = forge.aNullable { aString() } + ) + + // When + val resolvedStackTrace = errorEvent.resolveStacktrace() + assertThat(resolvedStackTrace).isNull() + } + + @Test + fun `M resolve the given kind W resolveKind { kind explicitly provided }`( + forge: Forge + ) { + // Given + val expectedKind = forge.aString() + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = null, + stacktrace = forge.aNullable { aString() }, + kind = expectedKind + ) + + // When + val resolvedKind = errorEvent.resolveKind() + assertThat(resolvedKind).isEqualTo(expectedKind) + } + + @Test + fun `M resolve the given kind W resolveKind { kind and throwable explicitly provided }`( + forge: Forge + ) { + // Given + val expectedKind = forge.aString() + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = forge.aThrowable(), + stacktrace = forge.aNullable { aString() }, + kind = expectedKind + ) + + // When + val resolvedKind = errorEvent.resolveKind() + assertThat(resolvedKind).isEqualTo(expectedKind) + } + + @Test + fun `M resolve throwable kind W resolveKind { only throwable explicitly provided }`( + forge: Forge + ) { + // Given + val fakeThrowable = forge.aThrowable() + val expectedKind = fakeThrowable.javaClass.canonicalName + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = fakeThrowable, + stacktrace = forge.aNullable { aString() }, + kind = null + ) + + // When + val resolvedKind = errorEvent.resolveKind() + assertThat(resolvedKind).isEqualTo(expectedKind) + } + + @Test + fun `M resolve throwable kind W resolveKind { only throwable explicitly provided, anonymous class }`( + forge: Forge + ) { + // Given + val fakeThrowable = object : Throwable() {} + val expectedKind = fakeThrowable.javaClass.simpleName + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = fakeThrowable, + stacktrace = forge.aNullable { aString() }, + kind = null + ) + + // When + val resolvedKind = errorEvent.resolveKind() + assertThat(resolvedKind).isEqualTo(expectedKind) + } + + @Test + fun `M resolve null W resolveKind { kind nor throwable provided }`( + forge: Forge + ) { + // Given + val errorEvent = InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = null, + stacktrace = forge.aNullable { aString() }, + kind = null + ) + + // When + val resolvedKind = errorEvent.resolveKind() + assertThat(resolvedKind).isNull() + } +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/ByteArrayExtTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/ByteArrayExtTest.kt new file mode 100644 index 0000000000..6166067798 --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/ByteArrayExtTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +class ByteArrayExtTest { + + @Test + fun `M return correct hex string W convert {0x00, 0x00}`() { + // Given + val byteArray = byteArrayOf(0x00, 0x00) // 0xA is 10 in decimal + + // When + val result = byteArray.toHexString() + + // Then + val expectedHex = "0000" + assertThat(result).isEqualTo(expectedHex) + } + + @Test + fun `M return correct hex string W convert {0xFF, 0xFF}`() { + // Given + val byteArray = byteArrayOf(0xFF.toByte(), 0xFF.toByte()) // 0xA is 10 in decimal + + // When + val result = byteArray.toHexString() + + // Then + val expectedHex = "ffff" + assertThat(result).isEqualTo(expectedHex) + } + + @Test + fun `M return correct hex string W call toHexString()`(@StringForgery fakeInput: String) { + // Given + val fakeByteArray = fakeInput.toByteArray() + + // When + val result = fakeByteArray.toHexString() + + // Then + val expected = fakeByteArray.joinToString(separator = "") { "%02x".format(Locale.US, it) } + assertThat(result).isEqualTo(expected) + } +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/DDCoreSubscriptionTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/DDCoreSubscriptionTest.kt new file mode 100644 index 0000000000..9af8470d99 --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/DDCoreSubscriptionTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@MockitoSettings(strictness = Strictness.LENIENT) +class DDCoreSubscriptionTest { + + @Mock + private lateinit var listener: TestListener + + private val subscription = DDCoreSubscription.create() + + @Test + fun `M notify listeners W notifyListeners {multiple listeners}`() { + // Given + val listener2 = mock() + + subscription.addListener(listener) + subscription.addListener(listener2) + + // When + subscription.notifyListeners { onSomethingChanged() } + + // Then + assertThat(subscription.listenersCount).isEqualTo(2) + inOrder(listener, listener2) { + verify(listener).onSomethingChanged() + verify(listener2).onSomethingChanged() + verifyNoMoreInteractions() + } + } + + @Test + fun `M remove listener W removeListener`() { + // Given + subscription.addListener(listener) + + // When + subscription.removeListener(listener) + subscription.notifyListeners { onSomethingChanged() } + + // Then + assertThat(subscription.listenersCount).isZero + verifyNoInteractions(listener) + } + + @Test + fun `M call all listeners W notifyListeners { if listener is removed during notifyListeners }`() { + // Given + val listener2 = mock() + whenever(listener2.onSomethingChanged()).doAnswer { + subscription.removeListener(listener) + } + + subscription.addListener(listener2) + subscription.addListener(listener) + + // When + subscription.notifyListeners { onSomethingChanged() } + + // Then + assertThat(subscription.listenersCount).isEqualTo(1) + inOrder(listener, listener2) { + verify(listener2).onSomethingChanged() + verify(listener).onSomethingChanged() + verifyNoMoreInteractions() + } + } + + @Test + fun `M not call a new listener W notifyListeners { if it is added during notifyListeners }`() { + // Given + val listener2 = mock() + whenever(listener2.onSomethingChanged()).doAnswer { + subscription.addListener(listener) + } + + subscription.addListener(listener2) + + // When + subscription.notifyListeners { onSomethingChanged() } + + // Then + assertThat(subscription.listenersCount).isEqualTo(2) + + verify(listener2).onSomethingChanged() + verifyNoMoreInteractions(listener, listener2) + } +} + +private interface TestListener { + fun onSomethingChanged() +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/StringBuilderExtKtTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/StringBuilderExtKtTest.kt new file mode 100644 index 0000000000..46de7f66d4 --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/StringBuilderExtKtTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions + +@Extensions( + ExtendWith(ForgeExtension::class) +) +internal class StringBuilderExtKtTest { + + @Test + fun `M add char W appendIfNotEmpty {buffer is not empty}`( + @StringForgery(regex = ".+") initialContent: String + ) { + // Given + val buffer = StringBuilder(initialContent) + + // When + buffer.appendIfNotEmpty(' ') + + // Then + assertThat(buffer.toString()).isEqualTo("$initialContent ") + } + + @Test + fun `M add str W appendIfNotEmpty {buffer is not empty}`( + @StringForgery(regex = ".+") initialContent: String + ) { + // Given + val buffer = StringBuilder(initialContent) + + // When + buffer.appendIfNotEmpty(" ") + + // Then + assertThat(buffer.toString()).isEqualTo("$initialContent ") + } + + @Test + fun `M not add any char W appendIfNotEmpty {buffer is empty}`() { + // Given + val buffer = StringBuilder() + + // When + buffer.appendIfNotEmpty(' ') + + // Then + assertThat(buffer.toString()).isEqualTo("") + } + + @Test + fun `M not add any str W appendIfNotEmpty {buffer is empty}`() { + // Given + val buffer = StringBuilder() + + // When + buffer.appendIfNotEmpty(" ") + + // Then + assertThat(buffer.toString()).isEqualTo("") + } +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/ThrowableExtTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/ThrowableExtTest.kt new file mode 100644 index 0000000000..91ce1b9e8b --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/internal/utils/ThrowableExtTest.kt @@ -0,0 +1,86 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.utils + +import com.datadog.android.internal.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import java.lang.RuntimeException + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +internal class ThrowableExtTest { + + @Forgery + lateinit var fakeThrowable: Throwable + + @Test + fun `M return stack trace W loggableStackTrace() {simple exception}`() { + // Given + val stack = fakeThrowable.stackTrace + + // When + val result = fakeThrowable.loggableStackTrace() + + // Then + val lines = result.lines() + assertThat(lines.first()).contains(fakeThrowable.message) + stack.forEachIndexed { i, frame -> + assertThat(lines[i + 1]) + .contains(frame.className) + .contains(frame.methodName) + } + } + + @Test + fun `M return stack trace W loggableStackTrace() {nested exception}`( + @StringForgery message: String + ) { + // Given + val topThrowable = RuntimeException(message, fakeThrowable) + val topStack = topThrowable.stackTrace + val stack = fakeThrowable.stackTrace + + // When + val result = topThrowable.loggableStackTrace() + + // Then + val lines = result.lines() + assertThat(lines.first()).contains(message) + topStack.forEachIndexed { i, frame -> + assertThat(lines[i + 1]) + .contains(frame.className) + .contains(frame.methodName) + } + + val offset = topStack.size + 1 + assertThat(lines.get(offset)) + .contains("Caused by") + .contains(fakeThrowable.message) + stack.forEachIndexed { i, frame -> + // When the "Caused by …" stacktrace has common frames with the previous one, + // those are not displayed and replaced with "… n more" + // In this test, there are at least 8 non common frames between the fakeThrowable + // and topThrowable + if (i < 8) { + assertThat(lines[i + offset + 1]) + .contains(frame.className) + .contains(frame.methodName) + } + } + } +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/android/rum/DdRumContentProviderTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/android/rum/DdRumContentProviderTest.kt new file mode 100644 index 0000000000..e55b561191 --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/android/rum/DdRumContentProviderTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum + +import android.app.ActivityManager +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Process +import com.datadog.android.internal.forge.Configurator +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.setFieldValue +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.net.URI + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class DdRumContentProviderTest { + + lateinit var testedProvider: ContentProvider + + @Mock + lateinit var mockContext: Context + + @Mock + lateinit var mockActivityManager: ActivityManager + + lateinit var fakeCurrentProcessInfo: ActivityManager.RunningAppProcessInfo + lateinit var fakeOtherProcessInfo: ActivityManager.RunningAppProcessInfo + + @BeforeEach + fun `set up`(forge: Forge) { + fakeCurrentProcessInfo = ActivityManager.RunningAppProcessInfo().apply { + this.processName = forge.anAlphabeticalString() + this.pid = Process.myPid() + this.importance = forge.anInt() + } + fakeOtherProcessInfo = ActivityManager.RunningAppProcessInfo().apply { + this.processName = forge.anAlphabeticalString() + this.pid = Process.myPid() + forge.aSmallInt() + this.importance = forge.anInt() + } + + whenever(mockContext.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( + mockActivityManager + ) + whenever(mockActivityManager.runningAppProcesses).thenReturn( + listOf(fakeCurrentProcessInfo, fakeOtherProcessInfo) + ) + + testedProvider = DdRumContentProvider() + testedProvider.setFieldValue("mContext", mockContext) + } + + @AfterEach + fun `tear down`() { + DdRumContentProvider.processImportance = 0 + } + + // region onCreate + + @Test + fun `M detect process importance W onCreate()`( + @IntForgery processImportance: Int + ) { + // Given + fakeCurrentProcessInfo.importance = processImportance + + // When + testedProvider.onCreate() + + // Then + assertThat(DdRumContentProvider.processImportance).isEqualTo(processImportance) + } + + @Test + fun `M detect process importance once W onCreate() twice`( + @IntForgery processImportance1: Int, + @IntForgery processImportance2: Int + ) { + // Given + assumeTrue(processImportance1 != processImportance2) + + // When + fakeCurrentProcessInfo.importance = processImportance1 + testedProvider.onCreate() + fakeCurrentProcessInfo.importance = processImportance2 + testedProvider.onCreate() + + // Then + assertThat(DdRumContentProvider.processImportance).isEqualTo(processImportance1) + } + + @Test + fun `M detect default process importance W onCreate() {no context}`() { + // Given + testedProvider.setFieldValue("mContext", null as Context?) + + // When + testedProvider.onCreate() + + // Then + assertThat(DdRumContentProvider.processImportance) + .isEqualTo(DdRumContentProvider.DEFAULT_IMPORTANCE) + } + + @Test + fun `M detect default process importance W onCreate() {no activity mgr}`() { + // Given + whenever(mockContext.getSystemService(Context.ACTIVITY_SERVICE)) doReturn null + + // When + testedProvider.onCreate() + + // Then + assertThat(DdRumContentProvider.processImportance) + .isEqualTo(DdRumContentProvider.DEFAULT_IMPORTANCE) + } + + // endregion + + // region ContentProvider + + @Test + fun `M return null W query()`( + @Forgery uri: URI, + @StringForgery projection: List, + @StringForgery selection: String, + @StringForgery selectionArgs: List, + @StringForgery sortOrder: String + ) { + // When + val cursor = testedProvider.query( + Uri.parse(uri.toString()), + projection.toTypedArray(), + selection, + selectionArgs.toTypedArray(), + sortOrder + ) + + // Then + assertThat(cursor).isNull() + } + + @Test + fun `M return null W getType()`( + @Forgery uri: URI + ) { + // When + val type = testedProvider.getType(Uri.parse(uri.toString())) + + // Then + assertThat(type).isNull() + } + + @Test + fun `M return null W insert()`( + @Forgery uri: URI + ) { + // When + val type = testedProvider.insert( + Uri.parse(uri.toString()), + ContentValues() + ) + + // Then + assertThat(type).isNull() + } + + @Test + fun `M return 0 W delete()`( + @Forgery uri: URI, + @StringForgery selection: String, + @StringForgery selectionArgs: List + ) { + // When + val deleted = testedProvider.delete( + Uri.parse(uri.toString()), + selection, + selectionArgs.toTypedArray() + ) + + // Then + assertThat(deleted).isZero() + } + + @Test + fun `M return 0 W update()`( + @Forgery uri: URI, + @StringForgery selection: String, + @StringForgery selectionArgs: List + ) { + // When + val deleted = testedProvider.update( + Uri.parse(uri.toString()), + ContentValues(), + selection, + selectionArgs.toTypedArray() + ) + + // Then + assertThat(deleted).isZero() + } + + // endregion +} diff --git a/dd-sdk-android-internal/src/test/java/com/datadog/internal/thread/NamedRunnableTest.kt b/dd-sdk-android-internal/src/test/java/com/datadog/internal/thread/NamedRunnableTest.kt new file mode 100644 index 0000000000..6f4e8856bb --- /dev/null +++ b/dd-sdk-android-internal/src/test/java/com/datadog/internal/thread/NamedRunnableTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.internal.thread + +import com.datadog.android.internal.thread.NamedRunnable +import com.datadog.tools.unit.forge.BaseConfigurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(value = BaseConfigurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +class NamedRunnableTest { + + @Test + fun `M execute the run() from original runnable W initialize with a runnable`(forge: Forge) { + // Given + val mockRunnable = mock() + val fakeName = forge.aString() + + // When + val testedNamedRunnable = NamedRunnable(fakeName, mockRunnable) + testedNamedRunnable.run() + + // Then + verify(mockRunnable).run() + verifyNoMoreInteractions(mockRunnable) + } + + @Test + fun `M return the sanitized name W given not sanitized name`(forge: Forge) { + // Given + val fakeSectionNumber = forge.anInt(1, 10) + val fakeStringList = mutableListOf() + val originalStringBuilder = StringBuilder() + repeat(fakeSectionNumber) { + val fakeName = forge.anAlphabeticalString() + originalStringBuilder.append(fakeName) + val symbol = forge.anElementFrom( + " ", + ",", + ".", + ":" + ) + originalStringBuilder.append(symbol) + fakeStringList.add(fakeName) + } + val expectedStringBuilder = StringBuilder() + repeat(fakeSectionNumber) { index -> + expectedStringBuilder.append(fakeStringList[index]) + expectedStringBuilder.append("_") + } + val mockRunnable = Mockito.mock() + + // When + val testedRunnable = NamedRunnable(originalStringBuilder.toString(), mockRunnable) + + // Then + assertThat(testedRunnable.name).isEqualTo(expectedStringBuilder.toString()) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryApiUsageForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryApiUsageForgeryFactory.kt new file mode 100644 index 0000000000..346645aa0a --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryApiUsageForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class InternalTelemetryApiUsageForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): InternalTelemetryEvent.ApiUsage { + return InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + overwrite = forge.aBool(), + noView = forge.aBool(), + noActiveView = forge.aBool(), + additionalProperties = forge.aMap { aString() to aString() }.toMutableMap() + ) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryConfigurationForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryConfigurationForgeryFactory.kt new file mode 100644 index 0000000000..657864a1c3 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryConfigurationForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class InternalTelemetryConfigurationForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): InternalTelemetryEvent.Configuration { + return InternalTelemetryEvent.Configuration( + trackErrors = forge.aBool(), + batchSize = forge.aLong(), + batchProcessingLevel = forge.anInt(), + batchUploadFrequency = forge.aLong(), + useProxy = forge.aBool(), + useLocalEncryption = forge.aBool() + ) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryDebugLogForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryDebugLogForgeryFactory.kt new file mode 100644 index 0000000000..3dbf817c33 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryDebugLogForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class InternalTelemetryDebugLogForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): InternalTelemetryEvent.Log.Debug { + return InternalTelemetryEvent.Log.Debug( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() } + ) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt new file mode 100644 index 0000000000..843bcc581c --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class InternalTelemetryErrorLogForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): InternalTelemetryEvent.Log.Error { + return InternalTelemetryEvent.Log.Error( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() }, + error = forge.aNullable { aThrowable() }, + stacktrace = forge.aNullable { aString() }, + kind = forge.aNullable { aString() } + ) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryEventForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryEventForgeryFactory.kt new file mode 100644 index 0000000000..d7dd04a200 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryEventForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class InternalTelemetryEventForgeryFactory : ForgeryFactory { + + @Suppress("MagicNumber") + override fun getForgery(forge: Forge): InternalTelemetryEvent { + val random = forge.anInt(min = 0, max = 6) + return when (random) { + 0 -> forge.getForgery() + + 1 -> forge.getForgery() + + 2 -> forge.getForgery() + 3 -> InternalTelemetryEvent.InterceptorInstantiated + 4 -> forge.getForgery() + else -> forge.getForgery() + } + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryMetricForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryMetricForgeryFactory.kt new file mode 100644 index 0000000000..e37a205900 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryMetricForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class InternalTelemetryMetricForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): InternalTelemetryEvent.Metric { + return InternalTelemetryEvent.Metric( + message = forge.aString(), + additionalProperties = forge.aMap { aString() to aString() } + ) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/TracingHeaderTypesSetForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/TracingHeaderTypesSetForgeryFactory.kt new file mode 100644 index 0000000000..9adcbf6990 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/TracingHeaderTypesSetForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.datadog.android.internal.telemetry.TracingHeaderType +import com.datadog.android.internal.telemetry.TracingHeaderTypesSet +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class TracingHeaderTypesSetForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): TracingHeaderTypesSet { + return TracingHeaderTypesSet( + types = forge.aList { + aValueFrom(TracingHeaderType::class.java) + }.toSet() + ) + } +} diff --git a/dd-sdk-android-internal/transitiveDependencies b/dd-sdk-android-internal/transitiveDependencies new file mode 100644 index 0000000000..85d3888238 --- /dev/null +++ b/dd-sdk-android-internal/transitiveDependencies @@ -0,0 +1,7 @@ +Dependencies List + +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 : 1706 Kb +org.jetbrains:annotations:13.0 : 17 Kb + +Total transitive dependencies size : 1723 Kb + diff --git a/dd-sdk-android-ktx/README.md b/dd-sdk-android-ktx/README.md deleted file mode 100644 index 1d2c692b7e..0000000000 --- a/dd-sdk-android-ktx/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Datadog extensions for Kotlin - -## Getting Started - -To include the Datadog extensions for Kotlin in your project, simply add the -following to your application's `build.gradle` file. - -``` -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:" - implementation "com.datadoghq:dd-sdk-android-ktx:" -} -``` - -### Initial Setup - -Before you can use the SDK, you need to setup the library with your application -context, your Client token and your Application ID. -To generate a Client token and an Application ID please check **UX Monitoring > RUM Applications > New Application** -in the Datadog dashboard. - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - val config = DatadogConfig.Builder("", "", "").build() - Datadog.initialize(this, config) - } -} -``` - -### Extension - -#### Running a lambda within a Span - -To monitor the performance of a given lambda, you can use the `withinSpan()` method. By default, a scope will be created for the span, but you can disable this behavior by setting the `activate` parameter to false. - -```kotlin - withinSpan("", parentSpan, activate) { - // Your code here - } -``` - -#### Span extension methods - -You can mark a span as having an error using one of the following `error()` methods. - -```kotlin - val span = tracer.buildSpan("").start() - try { - // … - } catch (e: IOException) { - span.error(e) - } - span.finish() -``` - -```kotlin - val span = tracer.buildSpan("").start() - if (invalidState) { - span.error("Something unexpected happened") - } - span.finish() -``` - -#### OkHttp Request extension method - -If you are using the `DatadogInterceptor` to trace your OkHttp requests, you can add a parent span using the `parentSpan()` method. - -```kotlin - val request = Request.Builder() - .parentSpan(span) - .build() -``` - -#### Tracing Coroutine code - -If you're using coroutines, you can trace the coroutine blocks using one of the following method. They behave like the usual coroutine methods, and simply require a span operation name. - -```kotlin - fun doSomething(){ - GlobalScope.launchTraced("", Dispatchers.IO) { - // … - } - - runBlockingTraced("", Dispatchers.IO) { - // … - } - } - - suspend fun coroutineMethod() { - val deferred = asyncTraced("", Dispatchers.IO) { - // … - } - - withContextTraced("", Dispatchers.Main) { - // … - } - - val result = deferred.awaitTraced("") - } -``` - - -#### Reporting Coroutine Flow errors - -If you're using Kotlin Coroutine Flow, you can propagate Flow errors to your RUM dashboard using the `sendErrorToDatadog()` method. - -```kotlin - suspend fun coroutineMethod() { - val flow = flow { emit(/*…*/) } - - flow.sendErrorToDatadog().collect { - // … - } - } -``` - -#### Tracing SQLite transaction - -If you are using SQLiteDatabase to persist data locally, you can trace the database transaction using the following method: - -```kotlin - sqliteDatabase.transactionTraced("",isExclusive){ database -> - // Your queries here - database.insert("", null, contentValues) - - // Decorate the Span - setTag("", "") - } -``` -It behaves like the `SQLiteDatabase.transaction` method provided in the `core-ktx` AndroidX package and only requires a span operation name. - -## Contributing - -Pull requests are welcome, but please open an issue first to discuss what you -would like to change. For more information, read the -[Contributing Guide](../CONTRIBUTING.md). - -## License - -[Apache License, v2.0](../LICENSE) diff --git a/dd-sdk-android-ktx/apiSurface b/dd-sdk-android-ktx/apiSurface deleted file mode 100644 index fcdaa31737..0000000000 --- a/dd-sdk-android-ktx/apiSurface +++ /dev/null @@ -1,16 +0,0 @@ -fun launchTraced(String, kotlin.coroutines.CoroutineContext = EmptyCoroutineContext, kotlinx.coroutines.CoroutineStart = CoroutineStart.DEFAULT, CoroutineScopeSpan.() -> Unit): kotlinx.coroutines.Job -fun runBlockingTraced(String, kotlin.coroutines.CoroutineContext = EmptyCoroutineContext, kotlinx.coroutines.CoroutineScope.() -> T): T -fun asyncTraced(String, kotlin.coroutines.CoroutineContext = EmptyCoroutineContext, kotlinx.coroutines.CoroutineStart = CoroutineStart.DEFAULT, CoroutineScopeSpan.() -> T): kotlinx.coroutines.Deferred -fun awaitTraced(String): T -fun withContextTraced(String, kotlin.coroutines.CoroutineContext, CoroutineScopeSpan.() -> T): T -interface com.datadog.android.ktx.coroutine.CoroutineScopeSpan : kotlinx.coroutines.CoroutineScope, io.opentracing.Span -fun sendErrorToDatadog(): kotlinx.coroutines.flow.Flow -fun useMonitored((T) -> R): R -fun getAssetAsRumResource(String, Int = AssetManager.ACCESS_STREAMING): java.io.InputStream -fun getRawResAsRumResource(Int): java.io.InputStream -fun asRumResource(String): java.io.InputStream -fun transactionTraced(String, Boolean = true, io.opentracing.Span.(android.database.sqlite.SQLiteDatabase) -> T): T -fun parentSpan(io.opentracing.Span): okhttp3.Request.Builder -fun setError(Throwable) -fun setError(String) -fun withinSpan(String, io.opentracing.Span? = null, Boolean = true, io.opentracing.Span.() -> T): T diff --git a/dd-sdk-android-ktx/build.gradle.kts b/dd-sdk-android-ktx/build.gradle.kts deleted file mode 100644 index 26b2d6a557..0000000000 --- a/dd-sdk-android-ktx/build.gradle.kts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-2019 Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.implementation -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - id("androidx.benchmark") - kotlin("android") - kotlin("android.extensions") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - isIgnoreTestSources = true - } -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.Coroutines) - implementation(Dependencies.Libraries.AndroidXAnnotation) - implementation(Dependencies.Libraries.OkHttp) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-ktx/src/main/AndroidManifest.xml b/dd-sdk-android-ktx/src/main/AndroidManifest.xml deleted file mode 100644 index dccc3582e8..0000000000 --- a/dd-sdk-android-ktx/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/CoroutineScopeSpan.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/CoroutineScopeSpan.kt deleted file mode 100644 index 8adf84e1e9..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/CoroutineScopeSpan.kt +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.coroutine - -import io.opentracing.Span -import kotlinx.coroutines.CoroutineScope - -/** - * An object that implements both [Span] and [CoroutineScope]. - */ -interface CoroutineScopeSpan : CoroutineScope, Span diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/CoroutineScopeSpanImpl.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/CoroutineScopeSpanImpl.kt deleted file mode 100644 index f172afe384..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/CoroutineScopeSpanImpl.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.coroutine - -import io.opentracing.Span -import kotlinx.coroutines.CoroutineScope - -internal class CoroutineScopeSpanImpl( - private val scope: CoroutineScope, - private val span: Span -) : CoroutineScopeSpan, - CoroutineScope by scope, - Span by span diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/FlowExt.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/FlowExt.kt deleted file mode 100644 index 2d1925a4a3..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/coroutine/FlowExt.kt +++ /dev/null @@ -1,37 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.coroutine - -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flow - -internal const val ERROR_FLOW: String = "Coroutine Flow error" - -/** - * Returns a [Flow] that will send a RUM Error event if this [Flow] emits an error. - * Note that the error will also be emitted by the returned [Flow]. - */ -@Suppress("TooGenericExceptionCaught") -fun Flow.sendErrorToDatadog(): Flow { - return flow { - try { - collect { value -> emit(value) } - } catch (e: Throwable) { - GlobalRum.get() - .addError( - ERROR_FLOW, - RumErrorSource.SOURCE, - e, - emptyMap() - ) - throw e - } - } -} diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/ClosableExt.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/ClosableExt.kt deleted file mode 100644 index 95da0a1c35..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/ClosableExt.kt +++ /dev/null @@ -1,42 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.rum - -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import java.io.Closeable - -internal const val CLOSABLE_ERROR_NESSAGE = "Error while using the closeable" - -/** - * Executes the given [block] function on this [Closeable] instance - * and then closes it down correctly whether an exception - * is thrown or not. - * This extension works exactly as the [Closeable.use] extension and in case the [block] will throw - * any exception this will be intercepted and propagated as a Rum error event. - * @param block a function to process this [Closeable] resource. - * @return the result of [block] function invoked on this resource. - */ -@Suppress("TooGenericExceptionCaught") -fun T.useMonitored(block: (T) -> R): R { - try { - return block(this) - } catch (e: Throwable) { - handleError(e) - throw e - } finally { - try { - close() - } catch (closeException: Throwable) { - handleError(closeException) - } - } -} - -private fun handleError(throwable: Throwable) { - GlobalRum.get().addError(CLOSABLE_ERROR_NESSAGE, RumErrorSource.SOURCE, throwable, emptyMap()) -} diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/ContextExt.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/ContextExt.kt deleted file mode 100644 index 7d34b23349..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/ContextExt.kt +++ /dev/null @@ -1,65 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.rum - -import android.content.Context -import android.content.res.AssetManager -import android.content.res.Resources -import androidx.annotation.RawRes -import com.datadog.android.rum.resource.RumResourceInputStream -import java.io.InputStream - -/** - * Open an asset, returning an InputStream to read its contents, tracked as a RUM Resource. - * - * This provides access to files that have been bundled with an application as assets -- that is, - * files placed into the "assets" directory. - * - * @param fileName The name of the asset to open. This name can be hierarchical. - * @param accessMode Desired access mode for retrieving the data. - * - * @return [InputStream] access to the asset data - * - * @see [AssetManager.ACCESS_UNKNOWN] - * @see [AssetManager.ACCESS_STREAMING] - * @see [AssetManager.ACCESS_RANDOM] - * @see [AssetManager.ACCESS_BUFFER] - */ -fun Context.getAssetAsRumResource( - fileName: String, - accessMode: Int = AssetManager.ACCESS_STREAMING -): InputStream { - return RumResourceInputStream( - assets.open(fileName, accessMode), - "assets://$fileName" - ) -} - -/** - * Open a data stream for reading a raw resource, tracked as a RUM Resource. - * - * This can only be used with resources whose value is the name of an asset file -- that is, - * it can be used to open drawable, sound, and raw resources; it will fail on string and color - * resources. - * - * @param id the resource identifier to open, as generated by the aapt tool. - * - * @return [InputStream] Access to the resource data. - * - */ -@Suppress("SwallowedException") -fun Context.getRawResAsRumResource( - @RawRes id: Int -): InputStream { - val resName = try { - resources.getResourceName(id) - } catch (e: Resources.NotFoundException) { - "res/0x${id.toString(16)}" - } - - return RumResourceInputStream(resources.openRawResource(id), resName) -} diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/InputStreamExt.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/InputStreamExt.kt deleted file mode 100644 index b9d0a3ea67..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/rum/InputStreamExt.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.rum - -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.resource.RumResourceInputStream -import java.io.InputStream - -/** - * Allow the [RumMonitor] to track this [InputStream] as a RUM Resource. - * - * @param url the url to be associated with this resource - */ -fun InputStream.asRumResource(url: String): InputStream { - return RumResourceInputStream(this, url) -} diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/sqlite/SqliteDatabaseExt.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/sqlite/SqliteDatabaseExt.kt deleted file mode 100644 index fa21b87421..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/sqlite/SqliteDatabaseExt.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.sqlite - -import android.database.sqlite.SQLiteDatabase -import com.datadog.android.ktx.tracing.withinSpan -import io.opentracing.Span -import io.opentracing.util.GlobalTracer - -/** - * Run [body] in a transaction marking it as successful if it completes without exception. - * A [io.opentracing.Span] will be created around the transaction and will be closed when the - * transaction finishes. - * - * @param operationName the name of the [Span] created around the transaction. - * @param exclusive Run in `EXCLUSIVE` mode when true, `IMMEDIATE` mode otherwise. - * @param body the code to be executed inside the transaction. - */ -inline fun SQLiteDatabase.transactionTraced( - operationName: String, - exclusive: Boolean = true, - body: Span.(SQLiteDatabase) -> T -): T { - val parentSpan = GlobalTracer.get().activeSpan() - withinSpan(operationName, parentSpan, true) { - if (exclusive) { - beginTransaction() - } else { - beginTransactionNonExclusive() - } - try { - val result = this.body(this@transactionTraced) - setTransactionSuccessful() - return result - } finally { - endTransaction() - } - } -} - -// endregion diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/tracing/OkHttpRequestExt.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/tracing/OkHttpRequestExt.kt deleted file mode 100644 index 1395e96201..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/tracing/OkHttpRequestExt.kt +++ /dev/null @@ -1,19 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.tracing - -import io.opentracing.Span -import okhttp3.Request - -/** - * Set the parent for the [Span] created around this OkHttp [Request]. - * @param span the parent [Span] - */ -fun Request.Builder.parentSpan(span: Span): Request.Builder { - tag(Span::class.java, span) - return this -} diff --git a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/tracing/SpanExt.kt b/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/tracing/SpanExt.kt deleted file mode 100644 index 97567177d9..0000000000 --- a/dd-sdk-android-ktx/src/main/kotlin/com/datadog/android/ktx/tracing/SpanExt.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.datadog.android.ktx.tracing - -import com.datadog.android.tracing.AndroidTracer -import io.opentracing.Span -import io.opentracing.util.GlobalTracer - -/** - * Helper method to attach a Throwable to this [Span]. - * The Throwable information (class name, message and stacktrace) will be added to - * this [Span] as standard Error Tags. - * @param throwable the [Throwable] you wan to log - */ -fun Span.setError(throwable: Throwable) { - AndroidTracer.logThrowable(this, throwable) -} - -/** - * Helper method to attach an error message to this [Span]. - * The error message will be added to this [Span] as a standard Error Tag. - * @param message the error message you want to attach. - */ -fun Span.setError(message: String) { - AndroidTracer.logErrorMessage(this, message) -} - -/** - * Wraps the provided lambda within a [Span]. - * @param operationName the name of the [Span] created around the lambda - * @param parentSpan the parent [Span] (default is `null`) - * @param activate whether the created [Span] should be made active for the current thread - * (default is `true`) - * @param block the lambda function traced by this newly created [Span] - * - */ -@SuppressWarnings("TooGenericExceptionCaught") -inline fun withinSpan( - operationName: String, - parentSpan: Span? = null, - activate: Boolean = true, - block: Span.() -> T -): T { - val tracer = GlobalTracer.get() - - val span = tracer.buildSpan(operationName) - .asChildOf(parentSpan) - .start() - - val scope = if (activate) tracer.activateSpan(span) else null - - return try { - span.block() - } catch (e: Throwable) { - span.setError(e) - throw e - } finally { - span.finish() - scope?.close() - } -} diff --git a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/coroutine/FlowExtTest.kt b/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/coroutine/FlowExtTest.kt deleted file mode 100644 index f88481dae0..0000000000 --- a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/coroutine/FlowExtTest.kt +++ /dev/null @@ -1,105 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.coroutine - -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.tools.unit.getStaticValue -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.lang.IllegalStateException -import java.util.concurrent.atomic.AtomicBoolean -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -class FlowExtTest { - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @BeforeEach - fun `set up`() { - GlobalRum.registerIfAbsent(mockRumMonitor) - } - - @AfterEach - fun `tear down`() { - val isRegistered: AtomicBoolean = GlobalRum::class.java.getStaticValue("isRegistered") - isRegistered.set(false) - } - - @Test - fun `M add RUM Error W flow emits error`( - @StringForgery message: String - ) { - // Given - val throwable = IllegalStateException(message) - val flow = flow { throw(throwable) } - - // When - assertThrows { - runBlocking { - withContext(Dispatchers.IO) { - flow.sendErrorToDatadog() - .collect { println(it) } - } - } - } - - // Then - verify(mockRumMonitor).addError( - ERROR_FLOW, - RumErrorSource.SOURCE, - throwable, - emptyMap() - ) - } - - @Test - fun `M doNothing W flow emits successfully`( - @StringForgery data: String, - @StringForgery message: String - ) { - // Given - val flow = flow { emit(data) } - - // When - val result = mutableListOf() - runBlocking { - withContext(Dispatchers.IO) { - flow.sendErrorToDatadog() - .collect { result.add(it) } - } - } - - // Then - assertThat(result).containsExactly(data) - verifyZeroInteractions(mockRumMonitor) - } -} diff --git a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/sqlite/SqliteDatabaseExtTest.kt b/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/sqlite/SqliteDatabaseExtTest.kt deleted file mode 100644 index d5492967eb..0000000000 --- a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/sqlite/SqliteDatabaseExtTest.kt +++ /dev/null @@ -1,243 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.sqlite - -import android.content.ContentValues -import android.database.sqlite.SQLiteDatabase -import com.datadog.android.ktx.utils.Configurator -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Scope -import io.opentracing.Span -import io.opentracing.Tracer -import io.opentracing.util.GlobalTracer -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(value = Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -class SqliteDatabaseExtTest { - - @Mock - lateinit var mockTracer: Tracer - - @Mock - lateinit var mockSpanBuilder: Tracer.SpanBuilder - - @Mock - lateinit var mockSpan: Span - - @Mock - lateinit var mockParentSpan: Span - - @Mock - lateinit var mockScope: Scope - - @StringForgery - lateinit var fakeOperationName: String - - @Mock - lateinit var mockDatabase: SQLiteDatabase - - @Forgery - lateinit var fakeException: Throwable - - @BeforeEach - fun `set up`() { - GlobalTracer.registerIfAbsent(mockTracer) - whenever(mockTracer.buildSpan(fakeOperationName)) doReturn mockSpanBuilder - whenever(mockTracer.activateSpan(mockSpan)) doReturn mockScope - whenever(mockSpanBuilder.start()) doReturn mockSpan - } - - @AfterEach - fun `tear down`() { - GlobalTracer::class.java.setStaticValue("isRegistered", false) - } - - @Test - fun `M create Span around transaction W transactionTraced() {exclusive = true}`() { - // GIVEN - whenever(mockTracer.activeSpan()) doReturn mockParentSpan - whenever(mockSpanBuilder.asChildOf(mockParentSpan)) doReturn mockSpanBuilder - var transactionExecuted = false - - // WHEN - transactionExecuted = mockDatabase.transactionTraced( - fakeOperationName, - true - ) { - true - } - - // THEN - assertThat(transactionExecuted).isTrue() - verify(mockSpanBuilder).asChildOf(mockParentSpan) - inOrder(mockSpan, mockScope) { - verify(mockSpan).finish() - verify(mockScope).close() - } - inOrder(mockDatabase) { - verify(mockDatabase).beginTransaction() - verify(mockDatabase).setTransactionSuccessful() - verify(mockDatabase).endTransaction() - } - } - - @Test - fun `M create Span around transaction W transactionTraced() {exclusive = false}`() { - // GIVEN - whenever(mockTracer.activeSpan()) doReturn mockParentSpan - whenever(mockSpanBuilder.asChildOf(mockParentSpan)) doReturn mockSpanBuilder - var transactionExecuted = false - - // WHEN - transactionExecuted = mockDatabase.transactionTraced( - fakeOperationName, - false - ) { - true - } - - // THEN - assertThat(transactionExecuted).isTrue() - verify(mockSpanBuilder).asChildOf(mockParentSpan) - inOrder(mockSpan, mockScope) { - verify(mockSpan).finish() - verify(mockScope).close() - } - inOrder(mockDatabase) { - verify(mockDatabase).beginTransactionNonExclusive() - verify(mockDatabase).setTransactionSuccessful() - verify(mockDatabase).endTransaction() - } - } - - @Test - fun `M create Span around transaction W transactionTraced() without parents`() { - // GIVEN - whenever(mockTracer.activeSpan()) doReturn null - whenever(mockSpanBuilder.asChildOf(null as Span?)) doReturn mockSpanBuilder - var transactionExecuted = false - - // WHEN - transactionExecuted = mockDatabase.transactionTraced( - fakeOperationName, - false - ) { - true - } - - // THEN - assertThat(transactionExecuted).isTrue() - verify(mockSpanBuilder).asChildOf(null as Span?) - inOrder(mockSpan, mockScope) { - verify(mockSpan).finish() - verify(mockScope).close() - } - inOrder(mockDatabase) { - verify(mockDatabase).beginTransactionNonExclusive() - verify(mockDatabase).setTransactionSuccessful() - verify(mockDatabase).endTransaction() - } - } - - @Test - fun `M close the Span around transaction W transactionTraced() throws exception`() { - // GIVEN - var caughtException: Throwable? = null - whenever(mockTracer.activeSpan()) doReturn null - whenever(mockSpanBuilder.asChildOf(null as Span?)) doReturn mockSpanBuilder - - // WHEN - try { - mockDatabase.transactionTraced(fakeOperationName) { - throw fakeException - } - } catch (e: Throwable) { - caughtException = fakeException - } - - // THEN - assertThat(caughtException).isEqualTo(fakeException) - verify(mockSpanBuilder).asChildOf(null as Span?) - inOrder(mockSpan, mockScope) { - verify(mockSpan).finish() - verify(mockScope).close() - } - inOrder(mockDatabase) { - verify(mockDatabase).beginTransaction() - verify(mockDatabase).endTransaction() - } - } - - @Test - fun `M decorate the Span from lambda W transactionTraced`(forge: Forge) { - // GIVEN - val fakeTagKey = forge.anAlphabeticalString() - val fakeTagValue = forge.anAlphabeticalString() - whenever(mockTracer.activeSpan()) doReturn mockParentSpan - whenever(mockSpanBuilder.asChildOf(mockParentSpan)) doReturn mockSpanBuilder - var transactionExecuted = false - - // WHEN - transactionExecuted = mockDatabase.transactionTraced( - fakeOperationName, - false - ) { - setTag(fakeTagKey, fakeTagValue) - true - } - - // THEN - assertThat(transactionExecuted).isTrue() - verify(mockSpan).setTag(fakeTagKey, fakeTagValue) - } - - @Test - fun `M execute the lambda on the SQLiteDatabase instance W transactionTraced`(forge: Forge) { - // GIVEN - val fakeTable = forge.anAlphabeticalString() - val contentValues = ContentValues() - whenever(mockTracer.activeSpan()) doReturn mockParentSpan - whenever(mockSpanBuilder.asChildOf(mockParentSpan)) doReturn mockSpanBuilder - var transactionExecuted = false - - // WHEN - transactionExecuted = mockDatabase.transactionTraced( - fakeOperationName, - false - ) { mockDatabase -> - mockDatabase.insert(fakeTable, null, contentValues) - true - } - - // THEN - assertThat(transactionExecuted).isTrue() - verify(mockDatabase).insert(fakeTable, null, contentValues) - } -} diff --git a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/CloseableExtTest.kt b/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/CloseableExtTest.kt deleted file mode 100644 index a50a8b7e94..0000000000 --- a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/CloseableExtTest.kt +++ /dev/null @@ -1,124 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.tracing - -import com.datadog.android.ktx.rum.CLOSABLE_ERROR_NESSAGE -import com.datadog.android.ktx.rum.useMonitored -import com.datadog.android.ktx.utils.Configurator -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.tools.unit.getStaticValue -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.Closeable -import java.util.concurrent.atomic.AtomicBoolean -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@ForgeConfiguration(value = Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -class CloseableExtTest { - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Forgery - lateinit var fakeException: Throwable - - @Mock - lateinit var testMockCloseable: Closeable - - @StringForgery - lateinit var fakeString: String - - @BeforeEach - fun `set up`() { - GlobalRum.registerIfAbsent(mockRumMonitor) - } - - @AfterEach - fun `tear down`() { - val isRegistered: AtomicBoolean = GlobalRum::class.java.getStaticValue("isRegistered") - isRegistered.set(false) - } - - @Test - fun `M send an error event W exception in the block`(forge: Forge) { - // GIVEN - var caughtException: Throwable? = null - - // WHEN - try { - testMockCloseable.useMonitored { - throw fakeException - } - } catch (e: Throwable) { - caughtException = e - } - - // THEN - assertThat(caughtException).isEqualTo(fakeException) - verify(mockRumMonitor).addError( - CLOSABLE_ERROR_NESSAGE, - RumErrorSource.SOURCE, - fakeException, - emptyMap() - ) - } - - @Test - fun `M close the closeable instance W exception in the block`(forge: Forge) { - // GIVEN - var caughtException: Throwable? = null - // WHEN - try { - testMockCloseable.useMonitored { - throw fakeException - } - } catch (e: Throwable) { - caughtException = e - } - - // THEN - assertThat(fakeException).isEqualTo(caughtException) - verify(testMockCloseable).close() - verifyNoMoreInteractions(testMockCloseable) - } - - @Test - fun `M close the closeable instance W no exception in the block`(forge: Forge) { - // WHEN - val returnedValue = testMockCloseable.useMonitored { - fakeString - } - - // THEN - assertThat(returnedValue).isEqualTo(fakeString) - verify(testMockCloseable).close() - verifyNoMoreInteractions(testMockCloseable) - } -} diff --git a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/OkHttpRequestExtTest.kt b/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/OkHttpRequestExtTest.kt deleted file mode 100644 index 8edca653f0..0000000000 --- a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/OkHttpRequestExtTest.kt +++ /dev/null @@ -1,41 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ktx.tracing - -import com.datadog.android.ktx.utils.Configurator -import com.nhaarman.mockitokotlin2.mock -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Span -import okhttp3.Request -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -class OkHttpRequestExtTest { - - @Test - fun `set the parentSpan through the Request builder`( - @RegexForgery("http://[a-z0-9_]{8}\\.[a-z]{3}/") fakeUrl: String - ) { - val parentSpan: Span = mock() - val request = Request.Builder().url(http://github.com/fakeUrl).parentSpan(parentSpan).build() - - assertThat(request.tag(Span::class.java)).isEqualTo(parentSpan) - } -} diff --git a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/SpanExtTest.kt b/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/SpanExtTest.kt deleted file mode 100644 index fc42fbb8f9..0000000000 --- a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/tracing/SpanExtTest.kt +++ /dev/null @@ -1,199 +0,0 @@ -package com.datadog.android.ktx.tracing - -import com.datadog.android.ktx.utils.Configurator -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Scope -import io.opentracing.Span -import io.opentracing.Tracer -import io.opentracing.log.Fields -import io.opentracing.util.GlobalTracer -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -class SpanExtTest { - - @Mock - lateinit var mockTracer: Tracer - - @Mock - lateinit var mockSpanBuilder: Tracer.SpanBuilder - - @Mock - lateinit var mockSpan: Span - - @Mock - lateinit var mockParentSpan: Span - - @Mock - lateinit var mockScope: Scope - - @StringForgery - lateinit var fakeOperationName: String - - @BeforeEach - fun `set up`() { - GlobalTracer.registerIfAbsent(mockTracer) - whenever(mockTracer.buildSpan(fakeOperationName)) doReturn mockSpanBuilder - whenever(mockTracer.activateSpan(mockSpan)) doReturn mockScope - whenever(mockSpanBuilder.asChildOf(mockParentSpan)) doReturn mockSpanBuilder - whenever(mockSpanBuilder.start()) doReturn mockSpan - } - - @AfterEach - fun `tear down`() { - GlobalTracer::class.java.setStaticValue("isRegistered", false) - } - - @Test - fun `M log throwable W setError(Throwable)`( - @Forgery throwable: Throwable - ) { - val mockSpan: Span = mock() - - mockSpan.setError(throwable) - - argumentCaptor>().apply { - verify(mockSpan).log(capture()) - assertThat(firstValue) - .containsEntry(Fields.ERROR_OBJECT, throwable) - .containsOnlyKeys(Fields.ERROR_OBJECT) - } - } - - @Test - fun `M log error message W setError(String)`( - @StringForgery message: String - ) { - val mockSpan: Span = mock() - - mockSpan.setError(message) - - argumentCaptor>().apply { - verify(mockSpan).log(capture()) - assertThat(firstValue) - .containsEntry(Fields.MESSAGE, message) - .containsOnlyKeys(Fields.MESSAGE) - } - } - - @Test - fun `M create span around lambda W withinSpan(name){}`( - @StringForgery operationName: String, - @LongForgery result: Long - ) { - var lambdaCalled = false - whenever(mockSpanBuilder.asChildOf(null as Span?)) doReturn mockSpanBuilder - - val callResult = withinSpan(fakeOperationName) { - lambdaCalled = true - result - } - - assertThat(lambdaCalled).isTrue() - assertThat(callResult).isEqualTo(result) - verify(mockSpanBuilder).asChildOf(null as Span?) - inOrder(mockSpan, mockScope) { - verify(mockSpan).finish() - verify(mockScope).close() - } - } - - @Test - fun `M create span and scope around lambda W withinSpan(name, parent){}`( - @StringForgery operationName: String, - @LongForgery result: Long - ) { - var lambdaCalled = false - - val callResult = withinSpan(fakeOperationName, mockParentSpan) { - lambdaCalled = true - result - } - - assertThat(lambdaCalled).isTrue() - assertThat(callResult).isEqualTo(result) - verify(mockSpanBuilder).asChildOf(mockParentSpan) - inOrder(mockSpan, mockScope) { - verify(mockSpan).finish() - verify(mockScope).close() - } - } - - @Test - fun `M create span and scope around lambda W withinSpan(name, parent){} throwing error`( - @StringForgery operationName: String, - @Forgery throwable: Throwable, - @LongForgery result: Long - ) { - var lambdaCalled = false - - val thrown = assertThrows { - withinSpan(fakeOperationName, mockParentSpan) { - lambdaCalled = true - throw throwable - } - } - - assertThat(thrown).isEqualTo(throwable) - assertThat(lambdaCalled).isTrue() - verify(mockSpanBuilder).asChildOf(mockParentSpan) - inOrder(mockSpan, mockScope) { - argumentCaptor>().apply { - verify(mockSpan).log(capture()) - assertThat(firstValue) - .containsEntry(Fields.ERROR_OBJECT, throwable) - .containsOnlyKeys(Fields.ERROR_OBJECT) - } - - verify(mockSpan).finish() - verify(mockScope).close() - } - } - - @Test - fun `M create span around lambda W withinSpan(name, parent, false){}`( - @LongForgery result: Long - ) { - var lambdaCalled = false - - val callResult = withinSpan(fakeOperationName, mockParentSpan, false) { - lambdaCalled = true - result - } - - assertThat(lambdaCalled).isTrue() - assertThat(callResult).isEqualTo(result) - verify(mockSpanBuilder).asChildOf(mockParentSpan) - inOrder(mockSpan) { - verify(mockSpan).finish() - } - verify(mockTracer, never()).activateSpan(mockSpan) - } -} diff --git a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/utils/Configurator.kt b/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/utils/Configurator.kt deleted file mode 100644 index a8d8ceefc1..0000000000 --- a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/utils/Configurator.kt +++ /dev/null @@ -1,19 +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 2016-2020 Datadog, Inc. - */ - -package com.datadog.android.ktx.utils - -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeConfigurator -import fr.xgouchet.elmyr.jvm.useJvmFactories - -internal class Configurator : - ForgeConfigurator { - override fun configure(forge: Forge) { - forge.addFactory(ThrowableForgeryFactory()) - forge.useJvmFactories() - } -} diff --git a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/utils/ThrowableForgeryFactory.kt b/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/utils/ThrowableForgeryFactory.kt deleted file mode 100644 index 3e5d782196..0000000000 --- a/dd-sdk-android-ktx/src/test/kotlin/com/datadog/android/ktx/utils/ThrowableForgeryFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.datadog.android.ktx.utils - -import com.datadog.tools.unit.forge.aThrowable -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeryFactory - -class ThrowableForgeryFactory : - ForgeryFactory { - override fun getForgery(forge: Forge): Throwable { - return forge.aThrowable() - } -} diff --git a/dd-sdk-android-ktx/transitiveDependencies b/dd-sdk-android-ktx/transitiveDependencies deleted file mode 100644 index cdd698ef42..0000000000 --- a/dd-sdk-android-ktx/transitiveDependencies +++ /dev/null @@ -1,14 +0,0 @@ -Dependencies List - -androidx.annotation:annotation:1.1.0 : 27 Kb -com.squareup.okhttp3:okhttp:3.12.6 : 413 Kb -com.squareup.okio:okio:1.15.0 : 86 Kb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 : 19 Kb -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.3.9 : 1629 Kb -org.jetbrains:annotations:13.0 : 17 Kb - -Total transitive dependencies size : 3 Mb - diff --git a/dd-sdk-android-ndk/CMakeLists.txt b/dd-sdk-android-ndk/CMakeLists.txt deleted file mode 100644 index fa8690ff6a..0000000000 --- a/dd-sdk-android-ndk/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -cmake_minimum_required(VERSION 3.10.2) -add_subdirectory(src/main/cpp) - -if(${CMAKE_BUILD_TYPE} STREQUAL Debug) - enable_testing() - add_subdirectory(src/test/cpp) -endif() diff --git a/dd-sdk-android-ndk/apiSurface b/dd-sdk-android-ndk/apiSurface deleted file mode 100644 index c14ee06861..0000000000 --- a/dd-sdk-android-ndk/apiSurface +++ /dev/null @@ -1,6 +0,0 @@ -class com.datadog.android.ndk.NdkCrashReportsPlugin : com.datadog.android.plugin.DatadogPlugin - override fun register(com.datadog.android.plugin.DatadogPluginConfig) - override fun unregister() - override fun onContextChanged(com.datadog.android.plugin.DatadogContext) - override fun onConsentUpdated(com.datadog.android.privacy.TrackingConsent, com.datadog.android.privacy.TrackingConsent) - companion object diff --git a/dd-sdk-android-ndk/build.gradle.kts b/dd-sdk-android-ndk/build.gradle.kts deleted file mode 100644 index b2ed50b9d6..0000000000 --- a/dd-sdk-android-ndk/build.gradle.kts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-2019 Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.androidTestImplementation -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - kotlin("android") - kotlin("android.extensions") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - multiDexEnabled = true - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - externalNativeBuild { - cmake { - cppFlags.add("-std=c++14") - } - } - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - isIgnoreTestSources = true - } - - externalNativeBuild { - cmake { - path = File("$projectDir/CMakeLists.txt") - version = Dependencies.Versions.CMakeVersion - } - } - - packagingOptions { - exclude("META-INF/LICENSE.md") - exclude("META-INF/LICENSE-notice.md") - } - - ndkVersion = Dependencies.Versions.NdkVersion -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.OkHttp) - implementation(Dependencies.Libraries.AndroidXMultidex) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - - androidTestImplementation(project(":tools:unit")) - androidTestImplementation(Dependencies.Libraries.IntegrationTests) - androidTestImplementation(Dependencies.Libraries.Gson) - androidTestImplementation(Dependencies.Libraries.AssertJ) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/AndroidManifest.xml b/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/AndroidManifest.xml deleted file mode 100644 index ab15717294..0000000000 --- a/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/NdkTests.kt b/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/NdkTests.kt deleted file mode 100644 index e0727fc45a..0000000000 --- a/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/NdkTests.kt +++ /dev/null @@ -1,233 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ndk - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.datadog.tools.unit.assertj.JsonObjectAssert.Companion.assertThat -import com.google.gson.JsonParser -import fr.xgouchet.elmyr.junit4.ForgeRule -import java.lang.RuntimeException -import java.nio.charset.Charset -import java.util.UUID -import org.assertj.core.api.Assertions -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -internal class NdkTests { - - @get:Rule - val forge = ForgeRule() - - @get:Rule - val temporaryFolder = TemporaryFolder() - - companion object { - init { - System.loadLibrary("datadog-native-lib") - System.loadLibrary("datadog-native-lib-test") - } - } - - @Test - fun ndkSuitTests() { - if (runNdkSuitTests() != 0) { - throw RuntimeException("NDK Suit tests failed") - } - } - - @Test - fun ndkStandaloneTests() { - if (runNdkStandaloneTests() != 0) { - throw RuntimeException("NDK Standalone tests failed") - } - } - - @Test - fun mustWriteAnErrorLog_whenHandlingSignal_whenConsentUpdatedToGranted() { - val signal = forge.anInt(min = 1, max = 32) - val appId = randomUUIDOrNull() - val sessionId = randomUUIDOrNull() - val viewId = randomUUIDOrNull() - val serviceName = forge.anAlphabeticalString(size = 50) - val env = forge.anAlphabeticalString(size = 50) - // we need to keep this this size because we are using a buffer of [30] size in c++ for - // the error.signal attribute - val signalName = forge.anAlphabeticalString(size = 20) - val signalErrorMessage = forge.anAlphabeticalString() - initNdkErrorHandler( - temporaryFolder.root.absolutePath, - serviceName, - env, - appId, - sessionId, - viewId - ) - updateTrackingConsent(1) - simulateSignalInterception( - signal, - signalName, - signalErrorMessage - ) - - // we need to give time to native part to write the file - // otherwise we will get into race condition issues - Thread.sleep(5000) - - // assert the log file - val inputStream = temporaryFolder.root.listFiles()?.first()?.inputStream() - inputStream?.use { - val jsonString = String(it.readBytes(), Charset.forName("utf-8")) - val jsonObject = JsonParser.parseString(jsonString).asJsonObject - assertThat(jsonObject).hasField("service", serviceName) - assertThat(jsonObject).hasField("ddtags", "env:$env") - assertThat(jsonObject).hasField("status", "emergency") - assertThat(jsonObject).hasField("message", "Native crash detected") - assertThat(jsonObject).hasField("error.message", signalErrorMessage) - assertThat(jsonObject).hasField("error.signal", "$signalName: $signal") - assertThat(jsonObject).hasField("error.kind", "Native") - assertThat(jsonObject).hasField("logger.name", "crash") - assertThat(jsonObject).hasNullableField("application_id", appId) - assertThat(jsonObject).hasNullableField("session_id", sessionId) - assertThat(jsonObject).hasNullableField("view.id", viewId) - } - } - - @Test - fun mustNotWriteAnyLog_whenHandlingSignal_whenConsentUpdatedToPending() { - val signal = forge.anInt(min = 1, max = 32) - val appId = randomUUIDOrNull() - val sessionId = randomUUIDOrNull() - val viewId = randomUUIDOrNull() - val serviceName = forge.anAlphabeticalString(size = 50) - val env = forge.anAlphabeticalString(size = 50) - // we need to keep this this size because we are using a buffer of [30] size in c++ for - // the error.signal attribute - val signalName = forge.anAlphabeticalString(size = 20) - val signalErrorMessage = forge.anAlphabeticalString() - initNdkErrorHandler( - temporaryFolder.root.absolutePath, - serviceName, - env, - appId, - sessionId, - viewId - ) - updateTrackingConsent(0) - simulateSignalInterception( - signal, - signalName, - signalErrorMessage - ) - - // we need to give time to native part to write the file - // otherwise we will get into race condition issues - Thread.sleep(5000) - - // assert the log file - Assertions.assertThat(temporaryFolder.root.listFiles()).isEmpty() - } - - @Test - fun mustNotWriteAnyLog_whenHandlingSignal_whenConsentUpdatedToNotGranted() { - val signal = forge.anInt(min = 1, max = 32) - val appId = randomUUIDOrNull() - val sessionId = randomUUIDOrNull() - val viewId = randomUUIDOrNull() - val serviceName = forge.anAlphabeticalString(size = 50) - val env = forge.anAlphabeticalString(size = 50) - // we need to keep this this size because we are using a buffer of [30] size in c++ for - // the error.signal attribute - val signalName = forge.anAlphabeticalString(size = 20) - val signalErrorMessage = forge.anAlphabeticalString() - initNdkErrorHandler( - temporaryFolder.root.absolutePath, - serviceName, - env, - appId, - sessionId, - viewId - ) - updateTrackingConsent(2) - simulateSignalInterception( - signal, - signalName, - signalErrorMessage - ) - - // we need to give time to native part to write the file - // otherwise we will get into race condition issues - Thread.sleep(5000) - - // assert the log file - Assertions.assertThat(temporaryFolder.root.listFiles()).isEmpty() - } - - // region NDK - - /** - * Will run the actual test suites on the NDK side. - * @return 0 if all the tests passed. - */ - private external fun runNdkSuitTests(): Int - - /** - * Will run the singular tests on the NDK side. - * @return 0 if all the tests passed. - */ - private external fun runNdkStandaloneTests(): Int - - /** - * Will initialize the NDK crash reporter. - * @param storageDir the storage directory for the reported crash logs - * @param serviceName the service name for the main context - * @param environment the environment name for the main context - * @param appId the application id for the rum context - * @param sessionId the session id to be passed into the rum context - * @param viewId the view id to be passed into the rum context - */ - private external fun initNdkErrorHandler( - storageDir: String, - serviceName: String, - environment: String, - appId: String?, - sessionId: String?, - viewId: String? - ) - - /** - * Simulate a signal interception into the NDK crash reporter. - * @param signal the signal id (between 1 and 32) - * @param signalName the signal name (e.g. SIGHUP, SIGINT, SIGILL, etc.) - * @param signalMessage the signal error message - */ - private external fun simulateSignalInterception( - signal: Int, - signalName: String, - signalMessage: String - ) - - /** - * Updates the tracking consent into the NDK crash reporter. - * @param consent as the tracking consent value (0 - PENDING, 1 - GRANTED, 2 - NOT-GRANTED) - */ - private external fun updateTrackingConsent( - consent: Int - ) - - // endregion - - // region Internal - - private fun randomUUIDOrNull(): String? { - return forge.aNullable { UUID.randomUUID().toString() } - } - - // endregion -} diff --git a/dd-sdk-android-ndk/src/main/AndroidManifest.xml b/dd-sdk-android-ndk/src/main/AndroidManifest.xml deleted file mode 100644 index 60ea5ee644..0000000000 --- a/dd-sdk-android-ndk/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/dd-sdk-android-ndk/src/main/cpp/CMakeLists.txt b/dd-sdk-android-ndk/src/main/cpp/CMakeLists.txt deleted file mode 100644 index adee0acb75..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,33 +0,0 @@ -cmake_minimum_required(VERSION 3.10.2) - -add_library( # Sets the name of the library. - datadog-native-lib - # Sets the library as a shared library. - SHARED - # Provides a relative path to your source file(s). - datadog-native-lib.cpp - datadog-native-lib.h - utils/signal-monitor.c - utils/signal-monitor.h - utils/fileutils.cpp - utils/fileutils.h - utils/stringutils.cpp - utils/stringutils.h - utils/datetime.cpp - utils/datetime.h - utils/backtrace-handler.cpp - utils/backtrace-handler.h) -find_library( # Sets the name of the path variable. - log-lib - # Specifies the name of the NDK library that - # you want CMake to locate. - log) -target_link_libraries( # Specifies the target library. - datadog-native-lib - # Links the target library to the log library - # included in the NDK. - ${log-lib}) -set_target_properties(datadog-native-lib - PROPERTIES - COMPILE_OPTIONS - -Werror -Wall -pedantic) diff --git a/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.cpp b/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.cpp deleted file mode 100644 index ddb2f73be4..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.cpp +++ /dev/null @@ -1,226 +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 2016-Present Datadog, Inc. - */ - -#include "datadog-native-lib.h" - -#include -#include -#include -#include -#include - -#include "android/log.h" -#include "utils/backtrace-handler.h" -#include "utils/datetime.h" -#include "utils/fileutils.h" -#include "utils/signal-monitor.h" -#include "utils/stringutils.h" - -typedef std::string string; - -static struct RumContext { - string application_id; - string session_id; - string view_id; - - RumContext() : application_id(), session_id(), view_id() {} - -} rum_context; - -static struct Context { - string generic_message; - string emergency_status; - string error_kind; - string logger_name; - string environment; - string service_name; - string storage_dir; - - Context() : - environment(), - service_name(), - storage_dir(), - generic_message("Native crash detected"), - emergency_status("emergency"), - error_kind("Native"), - logger_name("crash") { - } -} main_context; - - -static const char *LOG_TAG = "DatadogNdkCrashReporter"; -static pthread_mutex_t handler_mutex = PTHREAD_MUTEX_INITIALIZER; -static const uint8_t tracking_consent_pending = 0; -static const uint8_t tracking_consent_granted = 1; -static uint8_t tracking_consent = tracking_consent_pending; // 0 - PENDING, 1 - GRANTED, 2 - NOT-GRANTED - - -std::string get_serialized_log(int signal, - const char *signal_name, - const char *error_message, - const char *date, - const std::string backtrace) { - std::string serializedLog = "{ "; - if (!rum_context.application_id.empty()) { - serializedLog.append(R"("application_id": ")").append(rum_context.application_id) - .append("\","); - } - if (!rum_context.session_id.empty()) { - serializedLog.append(R"("session_id": ")").append(rum_context.session_id) - .append("\","); - } - if (!rum_context.view_id.empty()) { - serializedLog.append(R"("view.id": ")").append(rum_context.view_id) - .append("\","); - } - // these values are either constants or they are marked as NonNull in JVM so we do not have - // to check them here. - char tags[105]; // max 105 characters for the environment name - snprintf(tags, sizeof(tags), "env:%s", main_context.environment.c_str()); - serializedLog.append(R"("message": ")").append(main_context.generic_message).append("\","); - serializedLog.append(R"("service": ")").append(main_context.service_name).append("\","); - serializedLog.append(R"("logger.name": ")").append(main_context.logger_name).append("\","); - serializedLog.append(R"("ddtags": ")").append(tags).append("\","); - serializedLog.append(R"("status": ")").append(main_context.emergency_status).append("\","); - serializedLog.append(R"("date": ")").append(date).append("\","); - serializedLog.append(R"("error.stack": ")").append(backtrace).append("\","); - serializedLog.append(R"("error.message": ")").append(error_message).append("\","); - char formatted_signal_message[30]; - const size_t messageSize = - sizeof(formatted_signal_message) / sizeof(formatted_signal_message[0]); - snprintf(formatted_signal_message, - messageSize, "%s: %d", - signal_name, - signal); - serializedLog.append(R"("error.signal": ")").append(formatted_signal_message).append("\","); - serializedLog.append(R"("error.kind": ")").append(main_context.error_kind).append("\""); - serializedLog.append(" }"); - return serializedLog; -} - -void crash_signal_intercepted(int signal, const char *signal_name, const char *error_message) { - // sync everything - pthread_mutex_lock(&handler_mutex); - if (tracking_consent != tracking_consent_granted) { - pthread_mutex_unlock(&handler_mutex); - return; - } - if (main_context.storage_dir.empty()) { - __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, - "The crash reports storage directory file path was null"); - pthread_mutex_unlock(&handler_mutex); - return; - } - - // create crash reporting directory if it does not exist - if (!fileutils::create_dir_if_not_exists(main_context.storage_dir.c_str())) { - __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, - "Was unable to create the NDK reports storage directory: %s", - main_context.storage_dir.c_str()); - pthread_mutex_unlock(&handler_mutex); - return; - } - - // format the current GMT time - char date[100]; - const char *format = "%Y-%m-%d'T'%H:%M:%S.000Z"; - format_date(date, sizeof(date), format); - - // extract the generate_backtrace - std::string backtrace = backtrace::generate_backtrace(); - - // serialize the log - std::string serialized_log = get_serialized_log(signal, signal_name, error_message, date, - backtrace); - - // dump the log into a new file - char filename[200]; - // The ARM_32 processors will use an unsigned long long to represent the uint_64. We will pick the - // String format that fits both ARM_32 and ARM_64 (llu). - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wformat" - snprintf(filename, sizeof(filename), "%s/%llu", main_context.storage_dir.c_str(), - time_since_epoch()); - #pragma clang diagnostic pop - std::ofstream logs_file_output_stream(filename, std::ofstream::out | std::ofstream::app); - const char *text = serialized_log.c_str(); - if (logs_file_output_stream.is_open()) { - logs_file_output_stream << text << "\n"; - } - logs_file_output_stream.close(); - - pthread_mutex_unlock(&handler_mutex); -} - -void update_main_context(JNIEnv *env, - jstring storage_path, - jstring service_name, - jstring environment) { - using namespace stringutils; - pthread_mutex_lock(&handler_mutex); - main_context.storage_dir = copy_to_string(env, storage_path); - main_context.service_name = copy_to_string(env, service_name); - main_context.environment = copy_to_string(env, environment); - pthread_mutex_unlock(&handler_mutex); -} - -void update_rum_context(JNIEnv *env, - jstring application_id, - jstring session_id, - jstring view_id) { - using namespace stringutils; - pthread_mutex_lock(&handler_mutex); - rum_context.application_id = copy_to_string(env, application_id); - rum_context.session_id = copy_to_string(env, session_id); - rum_context.view_id = copy_to_string(env, view_id); - pthread_mutex_unlock(&handler_mutex); -} - -void update_tracking_consent(jint consent) { - tracking_consent = (uint8_t) consent; -} - - -/// Jni bindings -extern "C" JNIEXPORT void JNICALL -Java_com_datadog_android_ndk_NdkCrashReportsPlugin_registerSignalHandler( - JNIEnv *env, - jobject handler, - jstring storage_path, - jstring service_name, - jstring environment, - jint consent) { - - update_main_context(env, storage_path, service_name, environment); - update_tracking_consent(consent); - install_signal_handlers(); -} - - -extern "C" JNIEXPORT void JNICALL -Java_com_datadog_android_ndk_NdkCrashReportsPlugin_unregisterSignalHandler( - JNIEnv *env, - jobject /* this */) { - uninstall_signal_handlers(); -} - -extern "C" JNIEXPORT void JNICALL -Java_com_datadog_android_ndk_NdkCrashReportsPlugin_updateTrackingConsent( - JNIEnv *env, - jobject /* this */, - jint consent) { - update_tracking_consent(consent); -} - -extern "C" JNIEXPORT void JNICALL -Java_com_datadog_android_ndk_NdkCrashReportsPlugin_updateRumContext( - JNIEnv *env, - jobject /* this */, - jstring application_id, - jstring session_id, - jstring view_id) { - update_rum_context(env, application_id, session_id, view_id); -} \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.h b/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.h deleted file mode 100644 index 81b2ef4820..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.h +++ /dev/null @@ -1,34 +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 2016-Present Datadog, Inc. - */ - -#include - -#ifndef DATADOG_NATIVE_LIB_H -#define DATADOG_NATIVE_LIB_H - - -#ifdef __cplusplus -extern "C" { -#endif - -void update_main_context(JNIEnv *env, - jstring storage_path, - jstring service_name, - jstring environment); - -void update_rum_context(JNIEnv *env, - jstring application_id, - jstring session_id, - jstring view_id); - -void update_tracking_consent(jint consent); - -void crash_signal_intercepted(int signal, const char *signal_name, const char *error_message); - -#ifdef __cplusplus -} -#endif -#endif \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.cpp b/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.cpp deleted file mode 100644 index 6fbdddf1df..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.cpp +++ /dev/null @@ -1,118 +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 2016-Present Datadog, Inc. - */ - -#include "backtrace-handler.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -static const size_t STACK_SIZE = 30; - -struct BacktraceState { - uintptr_t *current; - uintptr_t *end; -}; - -namespace { - _Unwind_Reason_Code unwind_callback(struct _Unwind_Context *context, void *arg) { - auto *state = static_cast(arg); - uintptr_t pointer_to_stack_line = _Unwind_GetIP(context); - if (pointer_to_stack_line) { - // we have reached the end of the given buffer size so we stop the unwind loop - if (state->current == state->end) { - return _URC_END_OF_STACK; - } else { - // we set the state->current to current+1 and we set - // its value as the pointer of the current stack line - *state->current++ = pointer_to_stack_line; - } - } - return _URC_NO_REASON; - } - - size_t capture_backtrace(uintptr_t *buffer, size_t max) { - BacktraceState state = {buffer, buffer + max}; - // unwinds the backtrace and fills the buffer with stack lines addresses - _Unwind_Backtrace(unwind_callback, &state); - return state.current - buffer; - } - - std::string address_to_hexa(uintptr_t address) { - char address_as_hexa[20]; - // The ARM_32 processors will use an unsigned long long to represent a pointer so we will choose the - // String format that fits both ARM_32 and ARM_64 (lx). - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wformat" - std::snprintf(address_as_hexa, sizeof(address_as_hexa), "0x%lx", address); - #pragma clang diagnostic pop - return std::string(address_as_hexa); - } - - void get_info_from_address(const uintptr_t address, std::string *backtrace) { - backtrace->append(std::to_string(address)); - Dl_info info; - int fetch_info_success = dladdr(reinterpret_cast(address), &info); - if (fetch_info_success) { - - if (info.dli_fname) { - backtrace->append(" "); - backtrace->append(info.dli_fname); - } - - backtrace->append(" "); - backtrace->append(address_to_hexa(address)); - - if (info.dli_sname) { - backtrace->append(" "); - backtrace->append(info.dli_sname); - } - - if (info.dli_fbase) { - backtrace->append(" "); - backtrace->append("+"); - backtrace->append(" "); - const uintptr_t address_offset = - address - reinterpret_cast(info.dli_fbase); - backtrace->append(std::to_string(address_offset)); - } - - } - - backtrace->append("\\n"); - } - -} - -namespace backtrace { - - std::string generate_backtrace() { - // define the buffer which will hold pointers to stack memory addresses - uintptr_t buffer[STACK_SIZE]; - // we will now unwind the stack and capture all the memory addresses up to STACK_SIZE in - // the buffer - const size_t captured_stacksize = capture_backtrace(buffer, STACK_SIZE); - std::string backtrace; - for (size_t idx = 0; idx < captured_stacksize; ++idx) { - // we will iterate through all the stack addresses and translate each address in - // readable informationdsadsa - get_info_from_address(buffer[idx], &backtrace); - - } - return backtrace; - } - - -} - - diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.h b/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.h deleted file mode 100644 index 60397c0139..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.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 2016-Present Datadog, Inc. - */ - -#include - -#ifndef BACKTRACE_HANDLER_H -#define BACKTRACE_HANDLER_H - -namespace backtrace { - - std::string generate_backtrace(); -} - -#endif \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/datetime.cpp b/dd-sdk-android-ndk/src/main/cpp/utils/datetime.cpp deleted file mode 100644 index d3eb5f2150..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/utils/datetime.cpp +++ /dev/null @@ -1,25 +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 2016-Present Datadog, Inc. - */ - -#include "datetime.h" - -#include -#include -#include -#include -#include - -uint64_t time_since_epoch() { - using namespace std::chrono; - return duration_cast(system_clock::now().time_since_epoch()).count(); -} - -void format_date(char *buffer, size_t buffer_size, const char *format) { - using namespace std::chrono; - const std::time_t t = time(nullptr); - const struct tm *timeinfo = gmtime(&t); - strftime(buffer, buffer_size, format, timeinfo); -} \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/fileutils.cpp b/dd-sdk-android-ndk/src/main/cpp/utils/fileutils.cpp deleted file mode 100644 index f551b06e00..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/utils/fileutils.cpp +++ /dev/null @@ -1,24 +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 2016-Present Datadog, Inc. - */ - -#include "fileutils.h" - -#include -#include -#include - -namespace fileutils { - - bool create_dir_if_not_exists(const char *dirPath) { - if (opendir(dirPath) == nullptr && ENOENT == errno) { - // directory does not exist. We will create it. - return mkdir(dirPath, S_IRWXU) == 0; - } - - return true; // the directory was already there - } -} - diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.c b/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.c deleted file mode 100644 index cec4ff72b7..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.c +++ /dev/null @@ -1,283 +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 2016-Present Datadog, Inc. - */ - -#include "signal-monitor.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../datadog-native-lib.h" - -static const char *LOG_TAG = "DatadogNdkCrashReporter"; -static sigset_t signals_mask; -static stack_t signal_stack; - -/* the signal handler array */ -static struct sigaction *global_sigaction; - -/* the previous signal handler array */ -static struct sigaction *global_previous_sigaction; - -static pthread_t datadog_watchdog_thread; - -bool handlers_installed = false; - -// for testing purposes -#ifndef NDEBUG -int performed_install_ops = 0; -int performed_uninstall_ops = 0; -#endif - -void recordInstallOp() { -#ifndef NDEBUG - performed_install_ops++; -#endif -} - -void recordUninstallOp() { -#ifndef NDEBUG - performed_uninstall_ops++; -#endif -} - -/** - * Native signals which will be captured by the signal handler™ - */ - -struct signal { - int signal_value; - char *signal_name; - char *signal_error_message; -}; - -// TODO https://datadoghq.atlassian.net/browse/RUMM-576 -// We should use a Hashtable (Hashmap) here. For now I could not find something in C so probably -// will have to implement one or just use a pure array[key] -> value implementation even though -// this will take more memory. -static const struct signal handled_signals[] = { - {SIGILL, "SIGILL", "Illegal instruction"}, - {SIGBUS, "SIGBUS", "Bus error (bad memory access)"}, - {SIGFPE, "SIGFPE", "Floating-point exception"}, - {SIGABRT, "SIGABRT", "The process was terminated"}, - {SIGSEGV, "SIGSEGV", "Segmentation violation (invalid memory reference)"}, - {SIGQUIT, "SIGQUIT", "Application Not Responding"} -}; - -static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; - -size_t handled_signals_size() { return sizeof(handled_signals) / sizeof(handled_signals[0]); } - -// It may happen that the process is killed during this function execution. -// Therefore this function may never return. -void invoke_previous_handler(int signum, siginfo_t *info, void *user_context) { - - pthread_mutex_lock(&mutex); - - const size_t signals_array_size = handled_signals_size(); - for (int i = 0; i < signals_array_size; ++i) { - const int signal = handled_signals[i].signal_value; - if (signal == signum) { - struct sigaction previous = global_previous_sigaction[i]; - // From sigaction(2): - // > If act is non-zero, it specifies an action (SIG_DFL, SIG_IGN, or a - // handler routine) - if (previous.sa_flags & SA_SIGINFO) { - // This handler can handle signal number, info, and user context - // (POSIX). From sigaction(2): > If this bit is set, the handler - // function is assumed to be pointed to by the sa_sigaction member of - // struct sigaction and should match the proto- type shown above or as - // below in EXAMPLES. This bit should not be set when assigning SIG_DFL - // or SIG_IGN. - previous.sa_sigaction(signum, info, user_context); - } else if (previous.sa_handler == SIG_DFL) { - // raise to trigger the default handler. It cannot be called directly. - raise(signum); - - } else if (previous.sa_handler != SIG_IGN) { - // This handler can only handle to signal number (ANSI C) - void (*previous_handler)(int) = previous.sa_handler; - previous_handler(signum); - } - } - } - - pthread_mutex_unlock(&mutex); -} - -void handle_signal(int signum, siginfo_t *info, void *user_context) { - const size_t signals_array_size = handled_signals_size(); - for (int i = 0; i < signals_array_size; i++) { - const int signal = handled_signals[i].signal_value; - if (signal == signum) { - crash_signal_intercepted(signal, - handled_signals[i].signal_name, - handled_signals[i].signal_error_message); - break; - } - } - - // We need to uninstall our custom handlers otherwise we will go in a continuous loop when - // calling the prev handler (sigaction) with this signum. - uninstall_signal_handlers(); - invoke_previous_handler(signum, info, user_context); -} - -bool configure_signal_stack() { - static size_t stackSize = SIGSTKSZ * 2; - if ((signal_stack.ss_sp = calloc(1, stackSize)) == NULL) { - return false; - } - signal_stack.ss_size = stackSize; - signal_stack.ss_flags = 0; - if (sigaltstack(&signal_stack, 0) < 0) { - return false; - } - return true; -} - -bool configure_global_sigaction() { - global_sigaction = calloc(handled_signals_size(), sizeof(struct sigaction)); - if (global_sigaction == NULL) { - return false; - } - sigemptyset(&global_sigaction->sa_mask); - global_sigaction->sa_sigaction = handle_signal; - // we will use the SA_ONSTACK mask here to handle the signal in a freshly new stack. - global_sigaction->sa_flags = SA_SIGINFO | SA_ONSTACK; - - return true; -} - -// This function should be called inside a locked mutex for Thread safety -bool override_native_signal_handlers() { - if (handlers_installed) { - return false; - } - const size_t signals_array_size = handled_signals_size(); - global_previous_sigaction = calloc(signals_array_size, sizeof(struct sigaction)); - if (global_previous_sigaction == NULL) { - __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, "Was not able to initialise."); - return false; - } - for (int i = 0; i < signals_array_size; i++) { - const int signal = handled_signals[i].signal_value; - int success = sigaction(signal, global_sigaction, - &global_previous_sigaction[i]); - if (success != 0) { - __android_log_print(ANDROID_LOG_ERROR, - LOG_TAG, - "Was not able to catch the signal: %d", - signal); - } - } - handlers_installed = true; - recordInstallOp(); - return true; -} - -void *initialize_watchdog_thread() { - pthread_mutex_lock(&mutex); - override_native_signal_handlers(); - pthread_mutex_unlock(&mutex); - return NULL; -} - -bool start_watchdog_thread() { - bool success = false; - sigemptyset(&signals_mask); - // Main thread does not block by default the SIGQUIT signals so we will have to - // block it and start a new thread for signals handling. When starting a new thread - // from main thread the new one will take the signals mask of the parent. - sigaddset(&signals_mask, SIGQUIT); - if (pthread_sigmask(SIG_BLOCK, &signals_mask, NULL) == 0) { - if (pthread_create(&datadog_watchdog_thread, NULL, - initialize_watchdog_thread, NULL) != 0) { - __android_log_write(ANDROID_LOG_ERROR, - LOG_TAG, - "Was not able to create the watchdog thread"); - } - - // we restore the defaults on the main thread - if (pthread_sigmask(SIG_UNBLOCK, &signals_mask, NULL) != 0) { - __android_log_write(ANDROID_LOG_ERROR, - LOG_TAG, - "Was not able to restore the mask on SIGQUIT signal"); - } - success = true; - - } else { - __android_log_write(ANDROID_LOG_ERROR, - LOG_TAG, - "Was not able to mask SIGQUIT signal"); - } - - return success; -} - -bool try_to_install_handlers() { - if (handlers_installed) { - return true; - } - - return (configure_signal_stack() && - configure_global_sigaction() && - start_watchdog_thread()); -} - -bool install_signal_handlers() { - pthread_mutex_lock(&mutex); - bool installed = try_to_install_handlers(); - pthread_mutex_unlock(&mutex); - return installed; -} - -void free_up_memory() { - if (global_sigaction != NULL) { - free(global_sigaction); - } - if (global_previous_sigaction != NULL) { - free(global_previous_sigaction); - } - if (signal_stack.ss_sp != NULL) { - free(signal_stack.ss_sp); - } - global_sigaction = NULL; - global_previous_sigaction = NULL; - signal_stack.ss_sp = NULL; -} - -void uninstall_signal_handlers() { - pthread_mutex_lock(&mutex); - - if (!handlers_installed) { - pthread_mutex_unlock(&mutex); - return; - } - - const size_t signals_array_size = handled_signals_size(); - for (int i = 0; i < signals_array_size; i++) { - struct sigaction *prev_action = &global_previous_sigaction[i]; - if (prev_action) { - const int signal = handled_signals[i].signal_value; - sigaction(signal, prev_action, 0); - } - } - - free_up_memory(); - recordUninstallOp(); - handlers_installed = false; - pthread_mutex_unlock(&mutex); -} - diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/stringutils.cpp b/dd-sdk-android-ndk/src/main/cpp/utils/stringutils.cpp deleted file mode 100644 index 243b4c2b99..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/utils/stringutils.cpp +++ /dev/null @@ -1,27 +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 2016-Present Datadog, Inc. - */ - -#include "stringutils.h" - -#include -#include - -namespace stringutils { - - std::string copy_to_string(JNIEnv *env, jstring from) { - if (from == nullptr) { - return std::string(); - } - - const char *raw_str = env->GetStringUTFChars(from, 0); - - std::string result(raw_str); - - env->ReleaseStringUTFChars(from, raw_str); - - return result; - } -} diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/stringutils.h b/dd-sdk-android-ndk/src/main/cpp/utils/stringutils.h deleted file mode 100644 index 27c04c6ee4..0000000000 --- a/dd-sdk-android-ndk/src/main/cpp/utils/stringutils.h +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - - -#include -#include - - -#ifndef STRINGUTILS_H -#define STRINGUTILS_H - - -namespace stringutils { - std::string copy_to_string(JNIEnv *env, jstring from); -} - -#endif - diff --git a/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/NdkCrashReportsPlugin.kt b/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/NdkCrashReportsPlugin.kt deleted file mode 100644 index cae93f42ee..0000000000 --- a/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/NdkCrashReportsPlugin.kt +++ /dev/null @@ -1,122 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.ndk - -import android.util.Log -import com.datadog.android.plugin.DatadogContext -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.DatadogPluginConfig -import com.datadog.android.privacy.TrackingConsent -import java.io.File -import java.lang.NullPointerException - -/** - * An implementation of the [DatadogPlugin] which will allow to intercept and report the - * NDK crashes to our logs dashboard. - */ -@SuppressWarnings("TooGenericExceptionCaught") -class NdkCrashReportsPlugin : DatadogPlugin { - private var nativeLibraryLoaded = false - - init { - var exception: Throwable? = null - try { - System.loadLibrary("datadog-native-lib") - nativeLibraryLoaded = true - } catch (e: SecurityException) { - exception = e - } catch (e: NullPointerException) { - exception = e - } catch (e: UnsatisfiedLinkError) { - exception = e - } - exception?.let { - Log.e(TAG, ERROR_LOADING_NATIVE_MESSAGE, exception) - } - } - - // region Plugin - override fun register(config: DatadogPluginConfig) { - if (!nativeLibraryLoaded) { - return - } - val ndkCrashesDirs = - File( - config.context.filesDir.absolutePath + - File.separator + - config.featurePersistenceDirName - ) - registerSignalHandler( - ndkCrashesDirs.absolutePath, - config.serviceName, - config.envName, - consentToInt(config.trackingConsent) - ) - } - - override fun unregister() { - if (!nativeLibraryLoaded) { - return - } - unregisterSignalHandler() - } - - override fun onContextChanged(context: DatadogContext) { - // TODO: RUMM-637 Only update the rum context if the `bundleWithRum` config attribute is true - context.rum?.let { - updateRumContext(it.applicationId, it.sessionId, it.viewId) - } - } - - // endregion - - // region TrackingConsentProviderCallback - - override fun onConsentUpdated(previousConsent: TrackingConsent, newConsent: TrackingConsent) { - updateTrackingConsent(consentToInt(newConsent)) - } - - internal fun consentToInt(newConsent: TrackingConsent): Int { - return when (newConsent) { - TrackingConsent.PENDING -> TRACKING_CONSENT_PENDING - TrackingConsent.GRANTED -> TRACKING_CONSENT_GRANTED - else -> TRACKING_CONSENT_NOT_GRANTED - } - } - - // endregion - - // region NDK - - private external fun registerSignalHandler( - storagePath: String, - serviceName: String, - environment: String, - consent: Int - ) - - private external fun unregisterSignalHandler() - - private external fun updateRumContext( - applicationId: String?, - sessionId: String?, - viewId: String? - ) - - private external fun updateTrackingConsent(consent: Int) - - // endregion - - companion object { - private const val TAG: String = "NdkCrashReportsPlugin" - private const val ERROR_LOADING_NATIVE_MESSAGE: String = - "We could not load the native library" - internal const val TRACKING_CONSENT_PENDING = 0 - internal const val TRACKING_CONSENT_GRANTED = 1 - internal const val TRACKING_CONSENT_NOT_GRANTED = 2 - } -} diff --git a/dd-sdk-android-ndk/src/test/cpp/CMakeLists.txt b/dd-sdk-android-ndk/src/test/cpp/CMakeLists.txt deleted file mode 100644 index 8e6f2fde6d..0000000000 --- a/dd-sdk-android-ndk/src/test/cpp/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -include_directories( - ../../main/cpp - ../../main/utils - ../cpp/utils -) -add_library( - datadog-native-lib-test - SHARED - integration-tests.cpp - test_datetime_utils.cpp - test_generate_backtrace.cpp - test_signal_monitor.cpp - test_utils.cpp - test_utils.h -) -find_library( # Sets the name of the path variable. - log-lib - # Specifies the name of the NDK library that - # you want CMake to locate. - log) -target_link_libraries(datadog-native-lib-test datadog-native-lib ${log-lib}) \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/test/cpp/integration-tests.cpp b/dd-sdk-android-ndk/src/test/cpp/integration-tests.cpp deleted file mode 100644 index 5d5dd0069f..0000000000 --- a/dd-sdk-android-ndk/src/test/cpp/integration-tests.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include -#include - -#include -#include -#include "greatest/greatest.h" -#include "utils/stringutils.h" - -// override the GREATES_PRINTF macro to use the android logcat -#define GREATEST_FPRINTF(ignore, fmt, ...) __android_log_print(ANDROID_LOG_INFO, "DatadogNDKTests", fmt, ##__VA_ARGS__) - -SUITE (datetime_utils); - -SUITE (backtrace_generation); - -SUITE (signal_monitor); - - -GREATEST_MAIN_DEFS(); - -TEST copy_jni_env_to_string(JNIEnv *jniEnv) { - jstring s = jniEnv->NewStringUTF("test string"); - std::string copied_to = stringutils::copy_to_string(jniEnv, s); - ASSERT_STR_EQ("test string", copied_to.c_str()); - PASS(); -} - -TEST copy_jni_env_to_string_when_source_is_null(JNIEnv *jniEnv) { - std::string copied_to = stringutils::copy_to_string(jniEnv, nullptr); - ASSERT_STR_EQ("", copied_to.c_str()); - PASS(); -} - -int run_jni_env_dependent_tests(JNIEnv *env) { - int argc = 0; - char *argv[] = {}; - GREATEST_MAIN_BEGIN(); - RUN_TEST1(copy_jni_env_to_string, env); - RUN_TEST1(copy_jni_env_to_string_when_source_is_null, env); - GREATEST_MAIN_END(); -} - -int run_test_suites() { - int argc = 0; - char *argv[] = {}; - GREATEST_MAIN_BEGIN(); - RUN_SUITE(datetime_utils); - RUN_SUITE(signal_monitor); - // This test fails on Bitrise on the first backtrace line assertion even and was not able to - // detect why so far. My guess is related with Linux environment, I actually logged the line - // and checked the regEx on top and was passing locally. We will disable this test for now as - // the end to end integration test is passing successfully. - //RUN_SUITE(backtrace_generation); - GREATEST_MAIN_END(); -} - -void test_generate_log( - const int signal, - const char *signal_name, - const char *signal_error_message) { - crash_signal_intercepted(signal, signal_name, signal_error_message); -} - - -extern "C" JNIEXPORT int JNICALL -Java_com_datadog_android_ndk_NdkTests_runNdkSuitTests(JNIEnv *env, jobject) { - return run_test_suites(); -} - -extern "C" JNIEXPORT int JNICALL -Java_com_datadog_android_ndk_NdkTests_runNdkStandaloneTests(JNIEnv *env, jobject) { - return run_jni_env_dependent_tests(env); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_datadog_android_ndk_NdkTests_initNdkErrorHandler(JNIEnv *env, jobject thiz, - jstring storage_dir, - jstring service_name, - jstring env_name, - jstring app_id, - jstring session_id, - jstring view_id) { - - update_main_context(env, storage_dir, service_name, env_name); - update_rum_context(env, app_id, session_id, view_id); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_datadog_android_ndk_NdkTests_simulateSignalInterception(JNIEnv *env, jobject thiz, - jint signal, - jstring signal_name, - jstring signal_message) { - - const int c_signal = (int) signal; - const char *name = env->GetStringUTFChars(signal_name, 0); - const char *message = env->GetStringUTFChars(signal_message, 0); - test_generate_log(c_signal, name, message); - env->ReleaseStringUTFChars(signal_name, name); - env->ReleaseStringUTFChars(signal_message, message); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_datadog_android_ndk_NdkTests_updateTrackingConsent( - JNIEnv *env, - jobject /* this */, - jint consent) { - update_tracking_consent(consent); -} - - - diff --git a/dd-sdk-android-ndk/src/test/cpp/test_datetime_utils.cpp b/dd-sdk-android-ndk/src/test/cpp/test_datetime_utils.cpp deleted file mode 100644 index 66fd9b457f..0000000000 --- a/dd-sdk-android-ndk/src/test/cpp/test_datetime_utils.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include - -#include "greatest/greatest.h" -#include "utils/datetime.h" - -TEST test_generate_event_date_format(void) { - char buffer[100]; - format_date(buffer, sizeof(buffer) / sizeof(buffer[0]), "%Y-%m-%d'T'%H:%M:%S.000Z"); - ASSERT(std::regex_match(buffer, std::regex( - "[0-9]{4}-[0-9]{2}-[0-9]{2}'T'[0-9]{2}:[0-9]{2}:[0-9]{2}.000Z"))); - PASS(); -} - - -SUITE (datetime_utils) { - RUN_TEST(test_generate_event_date_format); -} diff --git a/dd-sdk-android-ndk/src/test/cpp/test_generate_backtrace.cpp b/dd-sdk-android-ndk/src/test/cpp/test_generate_backtrace.cpp deleted file mode 100644 index 4ca85717e0..0000000000 --- a/dd-sdk-android-ndk/src/test/cpp/test_generate_backtrace.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "test_utils.h" - -#include -#include -#include - -#include "greatest/greatest.h" -#include "utils/backtrace-handler.h" - -TEST test_generate_backtrace(void) { - std::string backtrace = backtrace::generate_backtrace(); - std::list backtrace_lines = testutils::split_backtrace_into_lines( - backtrace.c_str()); - // we don't know if the stack is big enough to cover the required max size of 30 - unsigned int lines_count = backtrace_lines.size(); - const char *regex = "(\\d+)(.*)0[xX][0-9a-fA-F]+(.*)"; - ASSERT(lines_count > 0 && lines_count <= 30); - for (auto it = backtrace_lines.begin(); it != backtrace_lines.end(); ++it) { - ASSERT(std::regex_match(it->c_str(), std::regex(regex))); - } - PASS(); -} - - -SUITE (backtrace_generation) { - RUN_TEST(test_generate_backtrace); -} diff --git a/dd-sdk-android-ndk/src/test/cpp/test_signal_monitor.cpp b/dd-sdk-android-ndk/src/test/cpp/test_signal_monitor.cpp deleted file mode 100644 index 908c9b8e23..0000000000 --- a/dd-sdk-android-ndk/src/test/cpp/test_signal_monitor.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include - -#include "greatest/greatest.h" -#include "utils/signal-monitor.h" - -extern bool handlers_installed; -#ifndef NDEBUG -extern int performed_install_ops; -extern int performed_uninstall_ops; -#endif - -void clear_tests() { -#ifndef NDEBUG - performed_install_ops = 0; - performed_uninstall_ops = 0; -#endif -} - -bool performedUninstall(int times) { -#ifndef NDEBUG - return performed_uninstall_ops == times; -#else - return true; -#endif -} - -bool performedInstall(int times) { -#ifndef NDEBUG - return performed_install_ops == times; -#else - return true; -#endif -} - -TEST installs_signal_handlers(void) { - install_signal_handlers(); - // give time for the install thread to finish - sleep(5); - ASSERT(handlers_installed); - uninstall_signal_handlers(); - clear_tests(); - PASS(); -} - -TEST calling_install_more_times_in_a_row_will_only_install_once(void) { - install_signal_handlers(); - install_signal_handlers(); - install_signal_handlers(); - // give time for the install thread to finish - sleep(5); - ASSERT(handlers_installed); - ASSERT(performedInstall(1)); - uninstall_signal_handlers(); - clear_tests(); - PASS(); -} - - -TEST uninstalls_signal_handlers(void) { - // given - install_signal_handlers(); - sleep(5); - // when - uninstall_signal_handlers(); - ASSERT_FALSE(handlers_installed); - clear_tests(); - PASS(); -} - -TEST calling_uninstall_more_times_in_a_row_will_only_uninstall_once(void) { - // given - install_signal_handlers(); - sleep(5); - // when - uninstall_signal_handlers(); - uninstall_signal_handlers(); - uninstall_signal_handlers(); - ASSERT_FALSE(handlers_installed); - ASSERT(performedUninstall(1)); - clear_tests(); - PASS(); -} - - -SUITE (signal_monitor) { - RUN_TEST(installs_signal_handlers); - RUN_TEST(calling_install_more_times_in_a_row_will_only_install_once); - RUN_TEST(uninstalls_signal_handlers); - RUN_TEST(calling_uninstall_more_times_in_a_row_will_only_uninstall_once); -} diff --git a/dd-sdk-android-ndk/src/test/cpp/test_utils.cpp b/dd-sdk-android-ndk/src/test/cpp/test_utils.cpp deleted file mode 100644 index 387cc02b64..0000000000 --- a/dd-sdk-android-ndk/src/test/cpp/test_utils.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "test_utils.h" - -#include -#include -#include - -namespace testutils { - - std::string substr(char *source, size_t size) { - char substr[size]; - strncpy(substr, source, size); - return std::string(substr); - } - - std::list split_backtrace_into_lines(const char *buffer) { - std::list to_return; - char *current_address = (char *) buffer; - int substr_length = 0; - char *start_substr_buffer = (char *) current_address; - for (char c = *current_address; c; c = *++current_address) { - // cannot match a single \n character as we specifically - // add them as separated escaped characters in the backtrace to match the Java one - if (c == 'n' && *(current_address - 1) == '\\') { - const std::string x = substr(start_substr_buffer, substr_length - 2); - to_return.push_back(x); - // we want to skip the 'n' character next time we do the substring - start_substr_buffer = (char *) (current_address + 1); - substr_length = 0; - } - substr_length++; - } - // add the last line if there was no split character at the end - if (start_substr_buffer < current_address) { - to_return.push_front(substr(start_substr_buffer, substr_length)); - } - - return to_return; - } - -} \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/test/kotlin/NdkCrashReportsPluginTest.kt b/dd-sdk-android-ndk/src/test/kotlin/NdkCrashReportsPluginTest.kt deleted file mode 100644 index 19b4f3accb..0000000000 --- a/dd-sdk-android-ndk/src/test/kotlin/NdkCrashReportsPluginTest.kt +++ /dev/null @@ -1,53 +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 2016-Present Datadog, Inc. - */ - -import com.datadog.android.ndk.NdkCrashReportsPlugin -import com.datadog.android.privacy.TrackingConsent -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class NdkCrashReportsPluginTest { - - lateinit var testedPlugin: NdkCrashReportsPlugin - - @BeforeEach - fun `set up`() { - testedPlugin = NdkCrashReportsPlugin() - } - - @Test - fun `M resolve to PENDING int state W consentToInt { PENDING }`() { - assertThat(testedPlugin.consentToInt(TrackingConsent.PENDING)).isEqualTo( - NdkCrashReportsPlugin.TRACKING_CONSENT_PENDING - ) - } - - @Test - fun `M resolve to GRANTED int state W consentToInt { GRANTED }`() { - assertThat(testedPlugin.consentToInt(TrackingConsent.GRANTED)).isEqualTo( - NdkCrashReportsPlugin.TRACKING_CONSENT_GRANTED - ) - } - - @Test - fun `M resolve to NOT_GRANTED int state W consentToInt { NOT_GRANTED }`() { - assertThat(testedPlugin.consentToInt(TrackingConsent.NOT_GRANTED)).isEqualTo( - NdkCrashReportsPlugin.TRACKING_CONSENT_NOT_GRANTED - ) - } -} diff --git a/dd-sdk-android-ndk/transitiveDependencies b/dd-sdk-android-ndk/transitiveDependencies deleted file mode 100644 index 6a9648161e..0000000000 --- a/dd-sdk-android-ndk/transitiveDependencies +++ /dev/null @@ -1,12 +0,0 @@ -Dependencies List - -androidx.multidex:multidex:2.0.1 : 26 Kb -com.squareup.okhttp3:okhttp:3.12.6 : 413 Kb -com.squareup.okio:okio:1.15.0 : 86 Kb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains:annotations:13.0 : 17 Kb - -Total transitive dependencies size : 2 Mb - diff --git a/dd-sdk-android-rx/README.md b/dd-sdk-android-rx/README.md deleted file mode 100644 index e21f0c03a9..0000000000 --- a/dd-sdk-android-rx/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Datadog Integration for RxJava - -## Getting Started - -To include the Datadog integration for [RxJava][1] in your project, simply add the -following to your application's `build.gradle` file. - -``` -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:" - implementation "com.datadoghq:dd-sdk-android-rx:" -} -``` - -### Initial Setup - -Before you can use the SDK, you need to setup the library with your application -context, your Client token and your Application ID. -To generate a Client token and an Application ID please check **UX Monitoring > RUM Applications > New Application** -in the Datadog dashboard. - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "").build() - Datadog.initialize(this, config) - - val monitor = RumMonitor.Builder().build() - GlobalRum.registerIfAbsent(monitor) - } -} -``` - -Following RxJava's [Generated API documentation][2], you just have to apply the `doOnError` operator on your `Observable`, -`Flowable`, `Single`, `Maybe` or `Completable` and pass an instance of `DatadogErrorConsumer`. - -Doing so will automatically intercept any Exception thrown in the upper stream by creating RUM Error events. - -**Note** : If you are using `Kotlin` in your codebase you could also use the extension: `sendErrorToDatadog()`. - -Java: - -```java - Observable.create{...} - .doOnError(new DatadogErrorConsumer()) - ... -``` - -Kotlin: - -```java - Observable.create{...} - .publishErrorsToRum() - ... -``` - -## Contributing - -Pull requests are welcome, but please open an issue first 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/ReactiveX/RxJava -[2]: https://github.com/ReactiveX/RxJava/wiki \ No newline at end of file diff --git a/dd-sdk-android-rx/apiSurface b/dd-sdk-android-rx/apiSurface deleted file mode 100644 index 90ba9f5198..0000000000 --- a/dd-sdk-android-rx/apiSurface +++ /dev/null @@ -1,8 +0,0 @@ -class com.datadog.android.rx.DatadogRumErrorConsumer : io.reactivex.rxjava3.functions.Consumer - override fun accept(Throwable) - companion object -fun sendErrorToDatadog(): io.reactivex.rxjava3.core.Observable -fun sendErrorToDatadog(): io.reactivex.rxjava3.core.Single -fun sendErrorToDatadog(): io.reactivex.rxjava3.core.Flowable -fun sendErrorToDatadog(): io.reactivex.rxjava3.core.Maybe -fun sendErrorToDatadog(): io.reactivex.rxjava3.core.Completable diff --git a/dd-sdk-android-rx/build.gradle.kts b/dd-sdk-android-rx/build.gradle.kts deleted file mode 100644 index 9bc4f7596e..0000000000 --- a/dd-sdk-android-rx/build.gradle.kts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-2019 Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.implementation -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - id("androidx.benchmark") - kotlin("android") - kotlin("android.extensions") - kotlin("kapt") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - } -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.OkHttp) - implementation(Dependencies.Libraries.RxJava) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-rx/src/main/AndroidManifest.xml b/dd-sdk-android-rx/src/main/AndroidManifest.xml deleted file mode 100644 index 5948c62aae..0000000000 --- a/dd-sdk-android-rx/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/dd-sdk-android-rx/src/main/kotlin/com/datadog/android/rx/DatadogRumErrorConsumer.kt b/dd-sdk-android-rx/src/main/kotlin/com/datadog/android/rx/DatadogRumErrorConsumer.kt deleted file mode 100644 index 1bcf958ed6..0000000000 --- a/dd-sdk-android-rx/src/main/kotlin/com/datadog/android/rx/DatadogRumErrorConsumer.kt +++ /dev/null @@ -1,29 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rx - -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import io.reactivex.rxjava3.functions.Consumer - -/** - * Provides an implementation of [Consumer] already set up to send relevant information - * to Datadog. - * - * It will automatically send RUM error events whenever a RxJava Stream throws any [Exception]. - */ -class DatadogRumErrorConsumer : Consumer { - - /** @inheritDoc */ - override fun accept(error: Throwable) { - GlobalRum.get().addError(REQUEST_ERROR_MESSAGE, RumErrorSource.SOURCE, error, emptyMap()) - } - - companion object { - internal const val REQUEST_ERROR_MESSAGE = "RxJava stream error" - } -} diff --git a/dd-sdk-android-rx/src/main/kotlin/com/datadog/android/rx/DatadogRxExt.kt b/dd-sdk-android-rx/src/main/kotlin/com/datadog/android/rx/DatadogRxExt.kt deleted file mode 100644 index 419d96df5b..0000000000 --- a/dd-sdk-android-rx/src/main/kotlin/com/datadog/android/rx/DatadogRxExt.kt +++ /dev/null @@ -1,58 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rx - -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single - -/** - * Returns an [Observable] that will send a RUM Error event - * if this [Observable] emits an error. - * Note that the error will also be emitted by the returned [Observable] - */ -fun Observable.sendErrorToDatadog(): Observable { - return this.doOnError(DatadogRumErrorConsumer()) -} - -/** - * Returns a [Single] that will send a RUM Error event - * if this [Single] emits an error. - * Note that the error will also be emitted by the returned [Single] - */ -fun Single.sendErrorToDatadog(): Single { - return this.doOnError(DatadogRumErrorConsumer()) -} - -/** - * Returns a [Flowable] that will send a RUM Error event - * if this [Flowable] emits an error. - * Note that the error will also be emitted by the returned [Flowable] - */ -fun Flowable.sendErrorToDatadog(): Flowable { - return this.doOnError(DatadogRumErrorConsumer()) -} - -/** - * Returns an [Maybe] that will send a RUM Error event - * if this [Maybe] emits an error. - * Note that the error will also be emitted by the returned [Maybe] - */ -fun Maybe.sendErrorToDatadog(): Maybe { - return this.doOnError(DatadogRumErrorConsumer()) -} - -/** - * Returns a [Completable] that will send a RUM Error event - * if this [Completable] emits an error. - * Note that the error will also be emitted by the returned [Completable] - */ -fun Completable.sendErrorToDatadog(): Completable { - return this.doOnError(DatadogRumErrorConsumer()) -} diff --git a/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/DatadogRumErrorConsumerTest.kt b/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/DatadogRumErrorConsumerTest.kt deleted file mode 100644 index e736d061b5..0000000000 --- a/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/DatadogRumErrorConsumerTest.kt +++ /dev/null @@ -1,73 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rx - -import com.datadog.android.fresco.utils.Configurator -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.tools.unit.getStaticValue -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.atomic.AtomicBoolean -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@ForgeConfiguration(value = Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -class DatadogRumErrorConsumerTest { - - lateinit var testedConsumer: DatadogRumErrorConsumer - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Forgery - lateinit var fakeException: Throwable - - @BeforeEach - fun `set up`() { - GlobalRum.registerIfAbsent(mockRumMonitor) - testedConsumer = DatadogRumErrorConsumer() - } - - @AfterEach - fun `tear down`() { - val isRegistered: AtomicBoolean = GlobalRum::class.java.getStaticValue("isRegistered") - isRegistered.set(false) - } - - @Test - fun `M send an error event W intercepting an exception`(forge: Forge) { - // WHEN - testedConsumer.accept(fakeException) - - // THEN - verify(mockRumMonitor).addError( - DatadogRumErrorConsumer.REQUEST_ERROR_MESSAGE, - RumErrorSource.SOURCE, - fakeException, - emptyMap() - ) - } -} diff --git a/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/utils/Configurator.kt b/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/utils/Configurator.kt deleted file mode 100644 index 43aa713cfa..0000000000 --- a/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/utils/Configurator.kt +++ /dev/null @@ -1,19 +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 2016-2020 Datadog, Inc. - */ - -package com.datadog.android.fresco.utils - -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeConfigurator -import fr.xgouchet.elmyr.jvm.useJvmFactories - -internal class Configurator : - ForgeConfigurator { - override fun configure(forge: Forge) { - forge.addFactory(ThrowableForgeryFactory()) - forge.useJvmFactories() - } -} diff --git a/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/utils/ThrowableForgeryFactory.kt b/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/utils/ThrowableForgeryFactory.kt deleted file mode 100644 index 28b87657f8..0000000000 --- a/dd-sdk-android-rx/src/test/kotlin/com/datadog/android/rx/utils/ThrowableForgeryFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.datadog.android.fresco.utils - -import com.datadog.tools.unit.forge.aThrowable -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeryFactory - -class ThrowableForgeryFactory : - ForgeryFactory { - override fun getForgery(forge: Forge): Throwable { - return forge.aThrowable() - } -} diff --git a/dd-sdk-android-rx/transitiveDependencies b/dd-sdk-android-rx/transitiveDependencies deleted file mode 100644 index a8a6827df9..0000000000 --- a/dd-sdk-android-rx/transitiveDependencies +++ /dev/null @@ -1,13 +0,0 @@ -Dependencies List - -com.squareup.okhttp3:okhttp:3.12.6 : 413 Kb -com.squareup.okio:okio:1.15.0 : 86 Kb -io.reactivex.rxjava3:rxjava:3.0.0 : 2 Mb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains:annotations:13.0 : 17 Kb -org.reactivestreams:reactive-streams:1.0.3 : 11 Kb - -Total transitive dependencies size : 4 Mb - diff --git a/dd-sdk-android-sqldelight/README.md b/dd-sdk-android-sqldelight/README.md deleted file mode 100644 index 38d5b55fb0..0000000000 --- a/dd-sdk-android-sqldelight/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Datadog Integration for SQLDelight - -## Getting Started - -To include the Datadog integration for [SQLDelight][1] in your project, simply add the -following to your application's `build.gradle` file. - -``` -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:" - implementation "com.datadoghq:dd-sdk-android-sqldelight:" -} -``` - -### Initial Setup - -Before you can use the SDK, you need to setup the library with your application -context, your Client token and your Application ID. -To generate a Client token and an Application ID please check **UX Monitoring > RUM Applications > New Application** -in the Datadog dashboard. - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - val config = DatadogConfig.Builder("", "", "").build() - Datadog.initialize(this, config) - - GlobalRum.registerIfAbsent(RumMonitor.Builder().build()) - } -} -``` - -Following SQLDelight's [Generated API documentation][1], you just have to provide the `DatadogSqliteCallback` in the -`AndroidSqliteDriver` constructor. - -Doing this detects whenever a database is corrupted and sends a relevant -RUM error event for it. - -```kotlin - val database = YourDatabase( - AndroidSqliteDriver( - YourDatabase.Schema, - context, - callback = DatadogSqliteCallback(YourDatabase.Schema) - )) -``` - -### Extension methods for traced transactions - -If you are using SQLDelight transactions, you can trace the transaction block using the following 2 methods: - -```kotlin - database.yourQuery.transactionTraced("", noEnclosing){ - // … - } -``` - -```kotlin - val result = database.yourQuery.transactionTracedWithResult("", noEnclosing){ - // … - } -``` - -They behave as the default methods (`transaction(noEnclosing,block)`, `transactionWithResult(noEnclosing,block`) and they simply require a span name as an -extra argument. - -## Contributing - -Pull requests are welcome, but please open an issue first 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://cashapp.github.io/sqldelight/android_sqlite/ diff --git a/dd-sdk-android-sqldelight/apiSurface b/dd-sdk-android-sqldelight/apiSurface deleted file mode 100644 index 27d6c1452b..0000000000 --- a/dd-sdk-android-sqldelight/apiSurface +++ /dev/null @@ -1,8 +0,0 @@ -class com.datadog.android.sqldelight.DatadogSqliteCallback : com.squareup.sqldelight.android.AndroidSqliteDriver.Callback - constructor(com.squareup.sqldelight.db.SqlDriver.Schema) - override fun onCorruption(androidx.sqlite.db.SupportSQLiteDatabase) - companion object -fun transactionTraced(String, Boolean = false, TransactionWithSpanAndWithoutReturn.() -> Unit) -fun transactionTracedWithResult(String, Boolean = false, TransactionWithSpanAndWithReturn.() -> R): R -interface com.datadog.android.sqldelight.TransactionWithSpanAndWithReturn : com.squareup.sqldelight.TransactionWithReturn, io.opentracing.Span -interface com.datadog.android.sqldelight.TransactionWithSpanAndWithoutReturn : com.squareup.sqldelight.TransactionWithoutReturn, io.opentracing.Span diff --git a/dd-sdk-android-sqldelight/build.gradle.kts b/dd-sdk-android-sqldelight/build.gradle.kts deleted file mode 100644 index 559205740c..0000000000 --- a/dd-sdk-android-sqldelight/build.gradle.kts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-2019 Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.implementation -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - id("androidx.benchmark") - kotlin("android") - kotlin("android.extensions") - kotlin("kapt") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - } -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.OkHttp) - implementation(Dependencies.Libraries.SQLDelight) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-sqldelight/src/main/AndroidManifest.xml b/dd-sdk-android-sqldelight/src/main/AndroidManifest.xml deleted file mode 100644 index 0ea1bf8c34..0000000000 --- a/dd-sdk-android-sqldelight/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/DatadogSqliteCallback.kt b/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/DatadogSqliteCallback.kt deleted file mode 100644 index 0eb337d143..0000000000 --- a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/DatadogSqliteCallback.kt +++ /dev/null @@ -1,48 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.sqldelight - -import androidx.sqlite.db.SupportSQLiteDatabase -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import com.squareup.sqldelight.android.AndroidSqliteDriver -import com.squareup.sqldelight.db.SqlDriver -import java.util.Locale - -/** - * Extends the [AndroidSqliteDriver.Callback] to intercept any Database corruption callback and to - * automatically send a RUM error event whenever this issue occurs. - * - * For more information [https://www.sqlite.org/howtocorrupt.html] - */ -class DatadogSqliteCallback(schema: SqlDriver.Schema) : AndroidSqliteDriver.Callback(schema) { - - /** @inheritDoc */ - override fun onCorruption(db: SupportSQLiteDatabase) { - super.onCorruption(db) - GlobalRum.get() - .addError( - String.format( - DATABASE_CORRUPTION_ERROR_MESSAGE, - db.path, - Locale.US - ), - RumErrorSource.SOURCE, - null, - mapOf( - RumAttributes.ERROR_DATABASE_PATH to db.path, - RumAttributes.ERROR_DATABASE_VERSION to db.version - ) - ) - } - - companion object { - internal const val DATABASE_CORRUPTION_ERROR_MESSAGE = - "Corruption reported by sqlite database: %s" - } -} diff --git a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/SqlDelightExt.kt b/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/SqlDelightExt.kt deleted file mode 100644 index 0dd50ac472..0000000000 --- a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/SqlDelightExt.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.sqldelight - -import com.datadog.android.tracing.AndroidTracer -import com.squareup.sqldelight.Transacter -import com.squareup.sqldelight.Transacter.Transaction -import com.squareup.sqldelight.TransactionWithReturn -import com.squareup.sqldelight.TransactionWithoutReturn -import io.opentracing.Span -import io.opentracing.util.GlobalTracer - -// region Public API - -/** - * Starts a [Transaction] and runs [body] in that transaction. - * A span will be created around the transaction code and sent to Datadog. - * - * @param operationName the name of the [Span] created around the coroutine code. - * @param noEnclosing in case we want the currently opened transaction to be automatically closed - * @param body the code to be executed inside the transaction - * @throws IllegalStateException if [noEnclosing] is true and there is already an active - * [Transaction] on this thread. - */ -fun T.transactionTraced( - operationName: String, - noEnclosing: Boolean = false, - body: TransactionWithSpanAndWithoutReturn.() -> Unit -) { - withinSpan(operationName, GlobalTracer.get().activeSpan()) { - transaction(noEnclosing = noEnclosing) { - body.invoke(TransactionWithSpanAndWithoutReturnImpl(this@withinSpan, this)) - } - } -} - -/** - * Starts a [Transaction] and runs [body] in that transaction. - * A span will be created around the transaction code and sent to Datadog. - * - * @param operationName the name of the [Span] created around the coroutine code. - * @param noEnclosing in case we want the currently opened transaction to be automatically closed - * @param body the code to be executed inside the transaction - * @throws IllegalStateException if [noEnclosing] is true and there is already an active - * [Transaction] on this thread. - */ -fun T.transactionTracedWithResult( - operationName: String, - noEnclosing: Boolean = false, - body: TransactionWithSpanAndWithReturn.() -> R -): R { - withinSpan(operationName, GlobalTracer.get().activeSpan()) { - return transactionWithResult(noEnclosing = noEnclosing) { - body.invoke(TransactionWithSpanAndWithReturnImpl(this@withinSpan, this)) - } - } -} - -// endregion - -// region Internals - -internal class TransactionWithSpanAndWithReturnImpl( - private val span: Span, - private val transaction: TransactionWithReturn -) : TransactionWithSpanAndWithReturn, Span by span, TransactionWithReturn by transaction - -internal class TransactionWithSpanAndWithoutReturnImpl( - private val span: Span, - private val transaction: TransactionWithoutReturn -) : TransactionWithSpanAndWithoutReturn, Span by span, TransactionWithoutReturn by transaction - -@Suppress("ThrowingInternalException", "TooGenericExceptionCaught") -internal inline fun withinSpan( - operationName: String, - parentSpan: Span? = null, - block: Span.() -> T -): T { - val tracer = GlobalTracer.get() - - val span = tracer.buildSpan(operationName) - .asChildOf(parentSpan) - .start() - - val scope = tracer.activateSpan(span) - - return try { - span.block() - } catch (e: Throwable) { - AndroidTracer.logThrowable(span, e) - throw e - } finally { - span.finish() - scope.close() - } -} - -// endregion diff --git a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/TransactionWithSpanAndWithReturn.kt b/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/TransactionWithSpanAndWithReturn.kt deleted file mode 100644 index e36806748e..0000000000 --- a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/TransactionWithSpanAndWithReturn.kt +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.sqldelight - -import com.squareup.sqldelight.TransactionWithReturn -import io.opentracing.Span - -/** - * An object that implements both [Span] and [TransactionWithReturn]. - */ -interface TransactionWithSpanAndWithReturn : TransactionWithReturn, Span diff --git a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/TransactionWithSpanAndWithoutReturn.kt b/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/TransactionWithSpanAndWithoutReturn.kt deleted file mode 100644 index ad0bfbfedc..0000000000 --- a/dd-sdk-android-sqldelight/src/main/kotlin/com/datadog/android/sqldelight/TransactionWithSpanAndWithoutReturn.kt +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.sqldelight - -import com.squareup.sqldelight.TransactionWithoutReturn -import io.opentracing.Span - -/** - * An object that implements both [Span] and [TransactionWithoutReturn]. - */ -interface TransactionWithSpanAndWithoutReturn : TransactionWithoutReturn, Span diff --git a/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/DatadogSqliteCallbackTest.kt b/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/DatadogSqliteCallbackTest.kt deleted file mode 100644 index 97868f7250..0000000000 --- a/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/DatadogSqliteCallbackTest.kt +++ /dev/null @@ -1,101 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rx - -import androidx.sqlite.db.SupportSQLiteDatabase -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.android.sqldelight.DatadogSqliteCallback -import com.datadog.tools.unit.getStaticValue -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Locale -import java.util.concurrent.atomic.AtomicBoolean -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@MockitoSettings(strictness = Strictness.LENIENT) -class DatadogSqliteCallbackTest { - - lateinit var testedSqliteCallback: DatadogSqliteCallback - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Mock - lateinit var mockSqliteDatabase: SupportSQLiteDatabase - - @RegexForgery("[a-z]/[a-z]") - lateinit var fakeDbPath: String - - @IntForgery - var fakeDbVersion: Int = 0 - - @BeforeEach - fun `set up`() { - GlobalRum.registerIfAbsent(mockRumMonitor) - testedSqliteCallback = DatadogSqliteCallback(mock()) - whenever(mockSqliteDatabase.path).thenReturn(fakeDbPath) - whenever(mockSqliteDatabase.version).thenReturn(fakeDbVersion) - } - - @AfterEach - fun `tear down`() { - val isRegistered: AtomicBoolean = GlobalRum::class.java.getStaticValue("isRegistered") - isRegistered.set(false) - } - - @Test - fun `M send an error event W intercepting a DB corruption`(forge: Forge) { - // WHEN - testedSqliteCallback.onCorruption(mockSqliteDatabase) - - // THEN - val argumentCaptor = argumentCaptor>() - verify(mockRumMonitor).addError( - eq( - String.format( - DatadogSqliteCallback.DATABASE_CORRUPTION_ERROR_MESSAGE, - fakeDbPath, - Locale.US - ) - ), - eq(RumErrorSource.SOURCE), - eq(null), - argumentCaptor.capture() - ) - assertThat(argumentCaptor.allValues).containsExactly( - mapOf( - RumAttributes.ERROR_DATABASE_PATH to fakeDbPath, - RumAttributes.ERROR_DATABASE_VERSION to fakeDbVersion - ) - ) - } -} diff --git a/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/utils/Configurator.kt b/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/utils/Configurator.kt deleted file mode 100644 index b3050bf061..0000000000 --- a/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/utils/Configurator.kt +++ /dev/null @@ -1,19 +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 2016-2020 Datadog, Inc. - */ - -package com.datadog.android.sqldelight.utils - -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeConfigurator -import fr.xgouchet.elmyr.jvm.useJvmFactories - -internal class Configurator : - ForgeConfigurator { - override fun configure(forge: Forge) { - forge.addFactory(ThrowableForgeryFactory()) - forge.useJvmFactories() - } -} diff --git a/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/utils/ThrowableForgeryFactory.kt b/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/utils/ThrowableForgeryFactory.kt deleted file mode 100644 index e50c2b6dc8..0000000000 --- a/dd-sdk-android-sqldelight/src/test/kotlin/com/datadog/android/sqldelight/utils/ThrowableForgeryFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.datadog.android.sqldelight.utils - -import com.datadog.tools.unit.forge.aThrowable -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeryFactory - -class ThrowableForgeryFactory : - ForgeryFactory { - override fun getForgery(forge: Forge): Throwable { - return forge.aThrowable() - } -} diff --git a/dd-sdk-android-sqldelight/transitiveDependencies b/dd-sdk-android-sqldelight/transitiveDependencies deleted file mode 100644 index ca1f40ae41..0000000000 --- a/dd-sdk-android-sqldelight/transitiveDependencies +++ /dev/null @@ -1,17 +0,0 @@ -Dependencies List - -androidx.annotation:annotation:1.1.0 : 27 Kb -androidx.sqlite:sqlite:2.1.0 : 10 Kb -com.squareup.okhttp3:okhttp:3.12.6 : 413 Kb -com.squareup.okio:okio:1.15.0 : 86 Kb -com.squareup.sqldelight:android-driver:1.4.3 : 22 Kb -com.squareup.sqldelight:runtime-jvm:1.4.3 : 42 Kb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.0 : 3 Kb -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.0 : 15 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains:annotations:13.0 : 17 Kb - -Total transitive dependencies size : 2 Mb - diff --git a/dd-sdk-android-timber/README.md b/dd-sdk-android-timber/README.md deleted file mode 100644 index 08a205e00b..0000000000 --- a/dd-sdk-android-timber/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Datadog Integration for Timber - -## Getting Started - -To include the Datadog integration for Timber in your project, simply add the -following to your application's `build.gradle` file. - -``` -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:" - implementation "com.datadoghq:dd-sdk-android-timber:" -} -``` - -### Initial Setup - -Before you can use the SDK, you need to setup the library with your application -context and your API token. You can create a token from the Integrations > API -in Datadog. **Make sure you create a key of type `Client Token`.** - -Once Datadog is initialized, you can then create a `Logger` instance using the -dedicated builder, and integrate it in Timber, as follows: - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - val config = DatadogConfig.Builder(BuildConfig.DD_CLIENT_TOKEN).build() - Datadog.initialize(this, config) - - val logger = Logger.Builder() - .setNetworkInfoEnabled(true) - .setLogcatLogsEnabled(true) - .setDatadogLogsEnabled(true) - .build(); - Timber.plant(DatadogTree(logger)) - } -} -``` - -That's it, now all your Timber logs will be sent to Datadog automatically. - -You can configure the logger's tags and attributes, as explained in the [Datadog Android log collection documentation](http://docs.datadoghq.com/logs/log_collection/android) - -## Contributing - -Pull requests are welcome, but please open an issue first to discuss what you -would like to change. For more information, read the -[Contributing Guide](../CONTRIBUTING.md). - -## License - -[Apache License, v2.0](../LICENSE) diff --git a/dd-sdk-android-timber/apiSurface b/dd-sdk-android-timber/apiSurface deleted file mode 100644 index f239a47a62..0000000000 --- a/dd-sdk-android-timber/apiSurface +++ /dev/null @@ -1,3 +0,0 @@ -class com.datadog.android.timber.DatadogTree : timber.log.Timber.Tree - constructor(com.datadog.android.log.Logger) - override fun log(Int, String?, String, Throwable?) diff --git a/dd-sdk-android-timber/build.gradle.kts b/dd-sdk-android-timber/build.gradle.kts deleted file mode 100644 index d05cd0431f..0000000000 --- a/dd-sdk-android-timber/build.gradle.kts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Unless explicitly stated otherwise all pomFilesList in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.testImplementation - -plugins { - id("com.android.library") - kotlin("android") - kotlin("android.extensions") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - jacoco -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - testOptions { - unitTests.isReturnDefaultValues = true - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - isIgnoreTestSources = true - } -} - -dependencies { - api(project(":dd-sdk-android")) - implementation(Dependencies.Libraries.Kotlin) - implementation(Dependencies.Libraries.Timber) - - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -kotlinConfig() -detektConfig() -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android-timber/src/main/AndroidManifest.xml b/dd-sdk-android-timber/src/main/AndroidManifest.xml deleted file mode 100644 index aba0591487..0000000000 --- a/dd-sdk-android-timber/src/main/AndroidManifest.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/dd-sdk-android-timber/src/main/kotlin/com/datadog/android/timber/DatadogTree.kt b/dd-sdk-android-timber/src/main/kotlin/com/datadog/android/timber/DatadogTree.kt deleted file mode 100644 index 659087948f..0000000000 --- a/dd-sdk-android-timber/src/main/kotlin/com/datadog/android/timber/DatadogTree.kt +++ /dev/null @@ -1,34 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.timber - -import com.datadog.android.log.Logger -import timber.log.Timber - -/** - * An implementation of a [Timber.Tree], forwarding all logs to the provided [Logger]. - * - * @param logger the logger to use with Timber. - */ -class DatadogTree( - private val logger: Logger -) : - Timber.Tree() { - - init { - logger.addTag("android:timber") - } - - override fun log( - priority: Int, - tag: String?, - message: String, - t: Throwable? - ) { - logger.log(priority, message, t) - } -} diff --git a/dd-sdk-android-timber/transitiveDependencies b/dd-sdk-android-timber/transitiveDependencies deleted file mode 100644 index ada0edea6d..0000000000 --- a/dd-sdk-android-timber/transitiveDependencies +++ /dev/null @@ -1,10 +0,0 @@ -Dependencies List - -com.jakewharton.timber:timber:4.7.1 : 21 Kb -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 : 8 Kb -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 : 186 Kb -org.jetbrains.kotlin:kotlin-stdlib:1.4.10 : 1452 Kb -org.jetbrains:annotations:16.0.1 : 18 Kb - -Total transitive dependencies size : 1686 Kb - diff --git a/dd-sdk-android/.gitignore b/dd-sdk-android/.gitignore deleted file mode 100644 index fe34a4da53..0000000000 --- a/dd-sdk-android/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -# Built application files -*.apk -*.ap_ -*.aab - -# Files for the ART/Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin/ -gen/ -out/ - -# Gradle files -build/ - -# Generated Poko -src/main/kotlin/com/datadog/android/rum/internal/domain/model/ \ No newline at end of file diff --git a/dd-sdk-android/README.md b/dd-sdk-android/README.md deleted file mode 100644 index 6aa0e941b8..0000000000 --- a/dd-sdk-android/README.md +++ /dev/null @@ -1,175 +0,0 @@ -# Datadog SDK for Android - -## Getting Started - -To include the Datadog SDK for Android in your project, simply add the following -to your application's `build.gradle` file. - -``` -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:" -} -``` - -### Initial Setup - -Before you can use the SDK, you need to setup the library with your application -context and your API token. You can create a token from the Integrations > API -in Datadog. **Make sure you create a key of type `Client Token`.** - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - Datadog.initialize(this, BuildConfig.DD_CLIENT_TOKEN) - } -} -``` - -### Setup for Europe - -If you're targetting our [Europe servers](https://datadoghq.eu), you can -initialize the library like this: - -```kotlin -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - Datadog.initialize(this, BuildConfig.DD_CLIENT_TOKEN, Datadog.DATADOG_EU) - } -} -``` - -### Logger Initialization - -You can create a `Logger` instance using the dedicated builder, as follows: - -```kotlin - logger = Logger.Builder() - .setNetworkInfoEnabled(true) - .setServiceName("com.example.app.android") - .setLogcatLogsEnabled(true) - .setDatadogLogsEnabled(true) - .setLoggerName("name") - .build(); -``` - -### Logging - -You can then send logs with the following methods, mimicking the ones available -in the Android Framework: - -```kotlin - logger.d("A debug message.") - logger.i("Some relevant information ?") - logger.w("An important warning…") - logger.e("An error was met!") - logger.wtf("What a Terrible Failure!") -``` - -### Logging Errors - -If you caught an exception and want to log it with a message, you can do so as -follow: - -```kotlin - try { - doSomething() - } catch (e : IOException) { - logger.e("Error while doing something", e) - } -``` - -> Note: All logging methods can have a throwable attached to them. - -### Adding context - -#### Tags - -Tags take the form of a single String, but can also represent key-value pairs when using a colon, and are -You can add tags to a specific logger as follows: - -```kotlin - // This will add a tag "build_type:debug" or "build_type:release" accordingly - logger.addTag("build_type", BuildConfig.BUILD_TYPE) - - // This will add a tag "android" - logger.addTag("android") -``` - -You can remove tags from a specific logger as follows: - -```kotlin - // This will remove any tag starting with "build_type:" - logger.removeTagsWithKey("build_type") - - // This will remove the tag "android" - logger.removeTag("android") -``` - -#### Attributes - -Attributes are always in the form of a key-value pair. The value can be any primitive, String or Date. -You can add attributes to a specific logger as follows: - -```kotlin - // This will add an attribute "version_code" with an integer value - logger.addAttribute("version_code", BuildConfig.VERSION_CODE) - // This will add an attribute "version_name" with a String value - logger.addAttribute("version_name", BuildConfig.VERSION_NAME) -``` - -You can remove attributes from a specific logger as follows: - -```kotlin - logger.removeAttribute("version_code") - logger.removeAttribute("version_name") -``` - -#### Local Attributes - -Sometimes, you might want to log a message with attributes only for that specific message. You can -do so by providing a map alongside the message, each entry being added as an attribute. - -```kotlin - logger.i("onPageStarted", attributes = mapOf("http.url", url)) -``` - -In Java you can do so as follows: -```java - mLogger.d( - "onPageStarted", - null, - new HashMap() {{ - put("http.url", url); - }} - ); -``` - -### Setting the Library's verbosity - -If you need to get information about the Library, you can set the verbosity -level as follows: - -```kotlin - Datadog.setVerbosity(Log.INFO) -``` - -All the internal messages in the library with a priority equal or higher than -the provided level will be logged to Android's LogCat. - -## Contributing - -Pull requests are welcome, but please open an issue first to discuss what you -would like to change. For more information, read the -[Contributing Guide](../CONTRIBUTING.md). - -## License - -[Apache License, v2.0](../LICENSE) diff --git a/dd-sdk-android/apiSurface b/dd-sdk-android/apiSurface deleted file mode 100644 index 1d47d46015..0000000000 --- a/dd-sdk-android/apiSurface +++ /dev/null @@ -1,350 +0,0 @@ -object com.datadog.android.Datadog - DEPRECATED const val DATADOG_US: String - DEPRECATED const val DATADOG_EU: String - DEPRECATED fun initialize(android.content.Context, DatadogConfig) - fun initialize(android.content.Context, com.datadog.android.privacy.TrackingConsent, DatadogConfig) - DEPRECATED fun setEndpointUrl(String, com.datadog.android.log.EndpointUpdateStrategy) - fun isInitialized(): Boolean - fun clearAllData() - fun setVerbosity(Int) - fun setTrackingConsent(com.datadog.android.privacy.TrackingConsent) - fun setUserInfo(String? = null, String? = null, String? = null, Map = emptyMap()) -class com.datadog.android.DatadogConfig - class Builder - constructor(String, String, java.util.UUID) - constructor(String, String) - constructor(String, String, String) - fun build(): DatadogConfig - fun setLogsEnabled(Boolean): Builder - fun setTracesEnabled(Boolean): Builder - fun setCrashReportsEnabled(Boolean): Builder - fun setRumEnabled(Boolean): Builder - fun setServiceName(String): Builder - DEPRECATED fun setEnvironmentName(String): Builder - fun setFirstPartyHosts(List): Builder - fun useEUEndpoints(): Builder - fun useUSEndpoints(): Builder - fun useGovEndpoints(): Builder - fun useCustomLogsEndpoint(String): Builder - fun useCustomTracesEndpoint(String): Builder - fun useCustomCrashReportsEndpoint(String): Builder - fun useCustomRumEndpoint(String): Builder - fun trackInteractions(Array = emptyArray()): Builder - fun useViewTrackingStrategy(com.datadog.android.rum.tracking.ViewTrackingStrategy): Builder - fun addPlugin(com.datadog.android.plugin.DatadogPlugin, com.datadog.android.plugin.Feature): Builder - fun sampleRumSessions(Float): Builder - companion object -object com.datadog.android.DatadogEndpoint - const val LOGS_US: String - const val LOGS_EU: String - const val LOGS_GOV: String - const val TRACES_US: String - const val TRACES_EU: String - const val TRACES_GOV: String - const val RUM_US: String - const val RUM_EU: String - const val RUM_GOV: String - const val NTP_0: String - const val NTP_1: String - const val NTP_2: String - const val NTP_3: String -class com.datadog.android.DatadogEventListener : okhttp3.EventListener - override fun callStart(okhttp3.Call) - override fun dnsStart(okhttp3.Call, String) - override fun dnsEnd(okhttp3.Call, String, MutableList) - override fun connectStart(okhttp3.Call, java.net.InetSocketAddress, java.net.Proxy) - override fun connectEnd(okhttp3.Call, java.net.InetSocketAddress, java.net.Proxy, okhttp3.Protocol?) - override fun secureConnectStart(okhttp3.Call) - override fun secureConnectEnd(okhttp3.Call, okhttp3.Handshake?) - override fun responseHeadersStart(okhttp3.Call) - override fun responseHeadersEnd(okhttp3.Call, okhttp3.Response) - override fun responseBodyStart(okhttp3.Call) - override fun responseBodyEnd(okhttp3.Call, Long) - override fun callEnd(okhttp3.Call) - override fun callFailed(okhttp3.Call, java.io.IOException) - class Factory : okhttp3.EventListener.Factory - override fun create(okhttp3.Call): okhttp3.EventListener -open class com.datadog.android.DatadogInterceptor : com.datadog.android.tracing.TracingInterceptor - constructor(List, com.datadog.android.tracing.TracedRequestListener = NoOpTracedRequestListener()) - override fun intercept(okhttp3.Interceptor.Chain): okhttp3.Response - override fun onRequestIntercepted(okhttp3.Request, io.opentracing.Span?, okhttp3.Response?, Throwable?) - companion object -DEPRECATED enum com.datadog.android.log.EndpointUpdateStrategy - - DISCARD_OLD_DATA - - SEND_OLD_DATA_TO_NEW_ENDPOINT -object com.datadog.android.log.LogAttributes - const val APPLICATION_PACKAGE: String - const val APPLICATION_VERSION: String - const val ENV: String - const val DATE: String - const val DB_INSTANCE: String - const val DB_OPERATION: String - const val DB_STATEMENT: String - const val DB_USER: String - const val DD_SPAN_ID: String - const val DD_TRACE_ID: String - const val DURATION: String - const val ERROR_KIND: String - const val ERROR_MESSAGE: String - const val ERROR_STACK: String - const val HOST: String - const val HTTP_METHOD: String - const val HTTP_REFERRER: String - const val HTTP_REQUEST_ID: String - const val HTTP_STATUS_CODE: String - const val HTTP_URL: String - const val HTTP_USERAGENT: String - const val HTTP_VERSION: String - const val LOGGER_METHOD_NAME: String - const val LOGGER_NAME: String - const val LOGGER_THREAD_NAME: String - const val LOGGER_VERSION: String - const val MESSAGE: String - const val NETWORK_CARRIER_ID: String - const val NETWORK_CARRIER_NAME: String - const val NETWORK_CLIENT_IP: String - const val NETWORK_CLIENT_PORT: String - const val NETWORK_CONNECTIVITY: String - const val NETWORK_DOWN_KBPS: String - const val NETWORK_SIGNAL_STRENGTH: String - const val NETWORK_UP_KBPS: String - const val RUM_APPLICATION_ID: String - const val RUM_SESSION_ID: String - const val RUM_VIEW_ID: String - const val SERVICE_NAME: String - const val SOURCE: String - const val STATUS: String - const val USR_EMAIL: String - const val USR_ID: String - const val USR_NAME: String -class com.datadog.android.log.Logger - fun v(String, Throwable? = null, Map = emptyMap()) - fun d(String, Throwable? = null, Map = emptyMap()) - fun i(String, Throwable? = null, Map = emptyMap()) - fun w(String, Throwable? = null, Map = emptyMap()) - fun e(String, Throwable? = null, Map = emptyMap()) - fun wtf(String, Throwable? = null, Map = emptyMap()) - fun log(Int, String, Throwable? = null, Map = emptyMap()) - class Builder - fun build(): Logger - fun setServiceName(String): Builder - fun setDatadogLogsEnabled(Boolean): Builder - fun setLogcatLogsEnabled(Boolean): Builder - fun setNetworkInfoEnabled(Boolean): Builder - fun setLoggerName(String): Builder - fun setBundleWithTraceEnabled(Boolean): Builder - fun setBundleWithRumEnabled(Boolean): Builder - fun setSampleRate(Float): Builder - fun addAttribute(String, Boolean) - fun addAttribute(String, Int) - fun addAttribute(String, Long) - fun addAttribute(String, Float) - fun addAttribute(String, Double) - fun addAttribute(String, String?) - fun addAttribute(String, java.util.Date?) - fun addAttribute(String, com.google.gson.JsonObject?) - fun addAttribute(String, com.google.gson.JsonArray?) - fun removeAttribute(String) - fun addTag(String, String) - fun addTag(String) - fun removeTag(String) - fun removeTagsWithKey(String) -class com.datadog.android.plugin.DatadogContext - constructor(DatadogRumContext? = null) -interface com.datadog.android.plugin.DatadogPlugin : com.datadog.android.privacy.TrackingConsentProviderCallback - fun register(DatadogPluginConfig) - fun unregister() - fun onContextChanged(DatadogContext) -sealed class com.datadog.android.plugin.DatadogPluginConfig - constructor(android.content.Context, String, String, String, com.datadog.android.privacy.TrackingConsent) -class com.datadog.android.plugin.DatadogRumContext - constructor(String? = null, String? = null, String? = null) -enum com.datadog.android.plugin.Feature - - LOG - - CRASH - - TRACE - - RUM -enum com.datadog.android.privacy.TrackingConsent - - GRANTED - - NOT_GRANTED - - PENDING -interface com.datadog.android.privacy.TrackingConsentProviderCallback - fun onConsentUpdated(TrackingConsent, TrackingConsent) -object com.datadog.android.rum.GlobalRum - fun isRegistered(): Boolean - fun registerIfAbsent(RumMonitor): Boolean - fun registerIfAbsent(java.util.concurrent.Callable): Boolean - fun get(): RumMonitor - fun addAttribute(String, Any?) - fun removeAttribute(String) -enum com.datadog.android.rum.RumActionType - - TAP - - SCROLL - - SWIPE - - CLICK - - CUSTOM -object com.datadog.android.rum.RumAttributes - const val APPLICATION_VERSION: String - const val ENV: String - const val SERVICE_NAME: String - const val SOURCE: String - const val SDK_VERSION: String - const val TRACE_ID: String - const val SPAN_ID: String - const val ERROR_RESOURCE_METHOD: String - const val ERROR_RESOURCE_STATUS_CODE: String - const val ERROR_RESOURCE_URL: String - const val ERROR_DATABASE_VERSION: String - const val ERROR_DATABASE_PATH: String - const val ACTION_TARGET_CLASS_NAME: String - const val ACTION_TARGET_TITLE: String - const val ACTION_TARGET_PARENT_INDEX: String - const val ACTION_TARGET_PARENT_CLASSNAME: String - const val ACTION_TARGET_PARENT_RESOURCE_ID: String - const val ACTION_TARGET_RESOURCE_ID: String - const val ACTION_GESTURE_DIRECTION: String - const val NETWORK_CARRIER_ID: String - const val NETWORK_CARRIER_NAME: String - const val NETWORK_CONNECTIVITY: String - const val NETWORK_DOWN_KBPS: String - const val NETWORK_SIGNAL_STRENGTH: String - const val NETWORK_UP_KBPS: String - const val NETWORK_BYTES_READ: String -enum com.datadog.android.rum.RumErrorSource - - NETWORK - - SOURCE - - CONSOLE - - LOGGER - - AGENT - - WEBVIEW -class com.datadog.android.rum.RumInterceptor : com.datadog.android.DatadogInterceptor -interface com.datadog.android.rum.RumMonitor - fun startView(Any, String, Map = emptyMap()) - fun stopView(Any, Map = emptyMap()) - fun addUserAction(RumActionType, String, Map) - fun startUserAction(RumActionType, String, Map) - fun stopUserAction(RumActionType, String, Map = emptyMap()) - fun startResource(String, String, String, Map = emptyMap()) - fun stopResource(String, Int?, Long?, RumResourceKind, Map) - fun stopResourceWithError(String, Int?, String, RumErrorSource, Throwable) - fun addError(String, RumErrorSource, Throwable?, Map) - fun addTiming(String) - class Builder - fun sampleRumSessions(Float): Builder - fun build(): RumMonitor - companion object -enum com.datadog.android.rum.RumResourceKind - constructor(String) - - BEACON - - FETCH - - XHR - - DOCUMENT - - UNKNOWN - - IMAGE - - JS - - FONT - - CSS - - MEDIA - - OTHER - companion object -class com.datadog.android.rum.resource.RumResourceInputStream : java.io.InputStream - constructor(java.io.InputStream, String) - override fun read(): Int - override fun read(ByteArray): Int - override fun read(ByteArray, Int, Int): Int - override fun available(): Int - override fun skip(Long): Long - override fun markSupported(): Boolean - override fun mark(Int) - override fun reset() - override fun close() - companion object -class com.datadog.android.rum.tracking.AcceptAllActivities : ComponentPredicate - override fun accept(android.app.Activity): Boolean -class com.datadog.android.rum.tracking.AcceptAllDefaultFragment : ComponentPredicate - override fun accept(android.app.Fragment): Boolean -class com.datadog.android.rum.tracking.AcceptAllSupportFragments : ComponentPredicate - override fun accept(androidx.fragment.app.Fragment): Boolean -abstract class com.datadog.android.rum.tracking.ActivityLifecycleTrackingStrategy : android.app.Application.ActivityLifecycleCallbacks, TrackingStrategy - override fun register(android.content.Context) - override fun unregister(android.content.Context?) - override fun onActivityPaused(android.app.Activity) - override fun onActivityStarted(android.app.Activity) - override fun onActivityDestroyed(android.app.Activity) - override fun onActivitySaveInstanceState(android.app.Activity, android.os.Bundle) - override fun onActivityStopped(android.app.Activity) - override fun onActivityCreated(android.app.Activity, android.os.Bundle?) - override fun onActivityResumed(android.app.Activity) - protected fun convertToRumAttributes(android.os.Bundle?): Map - companion object -class com.datadog.android.rum.tracking.ActivityViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy - constructor(Boolean, ComponentPredicate = AcceptAllActivities()) - override fun onActivityCreated(android.app.Activity, android.os.Bundle?) - override fun onActivityStarted(android.app.Activity) - override fun onActivityResumed(android.app.Activity) - override fun onActivityPostResumed(android.app.Activity) - override fun onActivityPaused(android.app.Activity) - override fun onActivityDestroyed(android.app.Activity) -interface com.datadog.android.rum.tracking.ComponentPredicate - fun accept(T): Boolean -class com.datadog.android.rum.tracking.FragmentViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy - constructor(Boolean, ComponentPredicate = AcceptAllSupportFragments(), ComponentPredicate = AcceptAllDefaultFragment()) - override fun onActivityStarted(android.app.Activity) - override fun onActivityStopped(android.app.Activity) -class com.datadog.android.rum.tracking.MixedViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy - constructor(Boolean, ComponentPredicate = AcceptAllActivities(), ComponentPredicate = AcceptAllSupportFragments(), ComponentPredicate = AcceptAllDefaultFragment()) - override fun onActivityCreated(android.app.Activity, android.os.Bundle?) - override fun onActivityStarted(android.app.Activity) - override fun onActivityResumed(android.app.Activity) - override fun onActivityPaused(android.app.Activity) - override fun onActivityStopped(android.app.Activity) - override fun onActivityDestroyed(android.app.Activity) -class com.datadog.android.rum.tracking.NavigationViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy, androidx.navigation.NavController.OnDestinationChangedListener - constructor(Int, Boolean) - override fun onActivityStarted(android.app.Activity) - override fun onActivityStopped(android.app.Activity) - override fun onActivityPaused(android.app.Activity) - override fun onDestinationChanged(androidx.navigation.NavController, androidx.navigation.NavDestination, android.os.Bundle?) - companion object -interface com.datadog.android.rum.tracking.TrackingStrategy - fun register(android.content.Context) - fun unregister(android.content.Context?) -interface com.datadog.android.rum.tracking.ViewAttributesProvider - fun extractAttributes(android.view.View, MutableMap) -interface com.datadog.android.rum.tracking.ViewTrackingStrategy : TrackingStrategy -open class com.datadog.android.rum.webview.RumWebChromeClient : android.webkit.WebChromeClient - constructor() - override fun onConsoleMessage(android.webkit.ConsoleMessage?): Boolean - companion object -open class com.datadog.android.rum.webview.RumWebViewClient : android.webkit.WebViewClient - override fun onPageStarted(android.webkit.WebView?, String?, android.graphics.Bitmap?) - override fun onPageFinished(android.webkit.WebView?, String?) - override fun onReceivedError(android.webkit.WebView?, Int, String?, String?) - override fun onReceivedError(android.webkit.WebView?, android.webkit.WebResourceRequest?, android.webkit.WebResourceError?) - override fun onReceivedHttpError(android.webkit.WebView?, android.webkit.WebResourceRequest?, android.webkit.WebResourceResponse?) - override fun onReceivedSslError(android.webkit.WebView?, android.webkit.SslErrorHandler?, android.net.http.SslError?) - companion object -class com.datadog.android.sqlite.DatadogDatabaseErrorHandler : android.database.DatabaseErrorHandler - constructor(android.database.DatabaseErrorHandler = DefaultDatabaseErrorHandler()) - override fun onCorruption(android.database.sqlite.SQLiteDatabase) - companion object -class com.datadog.android.tracing.AndroidTracer : com.datadog.opentracing.DDTracer - override fun buildSpan(String): DDSpanBuilder - class Builder - fun build(): AndroidTracer - fun setServiceName(String): Builder - fun setPartialFlushThreshold(Int): Builder - fun addGlobalTag(String, String): Builder - fun setBundleWithRumEnabled(Boolean): Builder - companion object - fun logThrowable(io.opentracing.Span, Throwable) - fun logErrorMessage(io.opentracing.Span, String) -interface com.datadog.android.tracing.TracedRequestListener - fun onRequestIntercepted(okhttp3.Request, io.opentracing.Span, okhttp3.Response?, Throwable?) -open class com.datadog.android.tracing.TracingInterceptor : okhttp3.Interceptor - DEPRECATED constructor(List, TracedRequestListener = NoOpTracedRequestListener()) - constructor(TracedRequestListener = NoOpTracedRequestListener()) - override fun intercept(okhttp3.Interceptor.Chain): okhttp3.Response - protected open fun onRequestIntercepted(okhttp3.Request, io.opentracing.Span?, okhttp3.Response?, Throwable?) - companion object -class com.datadog.tools.annotation.NoOpImplementation diff --git a/dd-sdk-android/build.gradle.kts b/dd-sdk-android/build.gradle.kts deleted file mode 100644 index 3efc144334..0000000000 --- a/dd-sdk-android/build.gradle.kts +++ /dev/null @@ -1,177 +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 2016-Present Datadog, Inc. - */ - -import com.datadog.gradle.Dependencies -import com.datadog.gradle.api -import com.datadog.gradle.config.AndroidConfig -import com.datadog.gradle.config.BuildConfigPropertiesKeys -import com.datadog.gradle.config.GradlePropertiesKeys -import com.datadog.gradle.config.bintrayConfig -import com.datadog.gradle.config.dependencyUpdateConfig -import com.datadog.gradle.config.detektConfig -import com.datadog.gradle.config.jacocoConfig -import com.datadog.gradle.config.javadocConfig -import com.datadog.gradle.config.junitConfig -import com.datadog.gradle.config.kotlinConfig -import com.datadog.gradle.config.ktLintConfig -import com.datadog.gradle.config.publishingConfig -import com.datadog.gradle.implementation -import com.datadog.gradle.testImplementation -import org.jetbrains.kotlin.kapt3.base.Kapt.kapt - -plugins { - id("com.android.library") - kotlin("android") - kotlin("android.extensions") - kotlin("kapt") - `maven-publish` - id("com.github.ben-manes.versions") - id("io.gitlab.arturbosch.detekt") - id("org.jlleitschuh.gradle.ktlint") - id("thirdPartyLicences") - id("apiSurface") - id("jsonschema2poko") - id("transitiveDependencies") - id("org.jetbrains.dokka") - id("com.jfrog.bintray") - id("de.mobilej.unmock") - jacoco -} - -fun isLogEnabledInRelease(): String { - return project.findProperty(GradlePropertiesKeys.FORCE_ENABLE_LOGCAT) as? String ?: "false" -} - -android { - compileSdkVersion(AndroidConfig.TARGET_SDK) - buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) - - defaultConfig { - minSdkVersion(AndroidConfig.MIN_SDK) - targetSdkVersion(AndroidConfig.TARGET_SDK) - versionCode = AndroidConfig.VERSION.code - versionName = AndroidConfig.VERSION.name - } - - sourceSets.named("main") { - java.srcDir("src/main/kotlin") - } - sourceSets.named("test") { - java.srcDir("src/test/kotlin") - } - sourceSets.named("androidTest") { - java.srcDir("src/androidTest/kotlin") - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - testOptions { - unitTests.apply { - isReturnDefaultValues = true - } - } - - buildTypes { - getByName("release") { - buildConfigField( - "Boolean", - BuildConfigPropertiesKeys.LOGCAT_ENABLED, - isLogEnabledInRelease() - ) - } - - getByName("debug") { - buildConfigField( - "Boolean", - BuildConfigPropertiesKeys.LOGCAT_ENABLED, - "true" - ) - } - } - - packagingOptions { - exclude("META-INF/jvm.kotlin_module") - exclude("META-INF/LICENSE.md") - exclude("META-INF/LICENSE-notice.md") - } - - lintOptions { - isWarningsAsErrors = true - isAbortOnError = true - isCheckReleaseBuilds = false - isCheckGeneratedSources = true - isIgnoreTestSources = true - } -} - -dependencies { - implementation(Dependencies.Libraries.Kotlin) - - // Network - implementation(Dependencies.Libraries.OkHttp) - implementation(Dependencies.Libraries.Gson) - implementation(Dependencies.Libraries.KronosNTP) - - // Android Instrumentation - implementation(Dependencies.Libraries.AndroidXCore) - implementation(Dependencies.Libraries.AndroidXNavigation) - implementation(Dependencies.Libraries.AndroidXRecyclerView) - implementation(Dependencies.Libraries.AndroidXWorkManager) - - // OpenTracing - api(Dependencies.Libraries.OpenTracing) - - // Generate NoOp implementations - kapt(project(":tools:noopfactory")) - - // Testing - testImplementation(project(":tools:unit")) - testImplementation(Dependencies.Libraries.JUnit5) - testImplementation(Dependencies.Libraries.TestTools) - testImplementation(Dependencies.Libraries.OkHttpMock) - unmock(Dependencies.Libraries.Robolectric) - - // Static Analysis - detekt(project(":tools:detekt")) - detekt(Dependencies.Libraries.DetektCli) -} - -unMock { - keep("android.os.BaseBundle") - keep("android.os.Bundle") - keep("android.os.Parcel") - keepStartingWith("com.android.internal.util.") - keepStartingWith("android.util.") - keep("android.content.ComponentName") -} - -apply(from = "clone_dd_trace.gradle.kts") -apply(from = "clone_rum_schema.gradle.kts") - -jsonSchema2Poko { - inputDirPath = "src/main/json" - targetPackageName = "com.datadog.android.rum.internal.domain.model" - ignoredFiles = arrayOf("_common-schema.json", "long_task-schema.json") - nameMapping = mapOf( - "action-schema.json" to "ActionEvent", - "error-schema.json" to "ErrorEvent", - "resource-schema.json" to "ResourceEvent", - "view-schema.json" to "ViewEvent" - ) -} - -kotlinConfig() -detektConfig(excludes = listOf("**/com/datadog/android/rum/internal/domain/model/**")) -ktLintConfig() -junitConfig() -jacocoConfig() -javadocConfig() -dependencyUpdateConfig() -publishingConfig("${rootDir.canonicalPath}/repo") -bintrayConfig() diff --git a/dd-sdk-android/src/main/AndroidManifest.xml b/dd-sdk-android/src/main/AndroidManifest.xml deleted file mode 100644 index 2e9a0a0397..0000000000 --- a/dd-sdk-android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDSpan.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/DDSpan.java deleted file mode 100644 index 2d3f4e6758..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDSpan.java +++ /dev/null @@ -1,397 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import com.datadog.trace.api.DDTags; -import com.datadog.trace.api.interceptor.MutableSpan; -import com.datadog.trace.api.sampling.PrioritySampling; -import com.datadog.trace.common.util.Clock; -import io.opentracing.Span; -import io.opentracing.tag.Tag; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.ref.WeakReference; -import java.math.BigInteger; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Represents a period of time. Associated information is stored in the SpanContext. - * - *

Spans are created by the {@link DDTracer#buildSpan}. This implementation adds some features - * according to the DD agent. - */ -public class DDSpan implements Span, MutableSpan { - - /** The context attached to the span */ - private final DDSpanContext context; - - /** - * Creation time of the span in microseconds provided by external clock. Must be greater than - * zero. - */ - private final long startTimeMicro; - - /** - * Creation time of span in nanoseconds. We use combination of millisecond-precision clock and - * nanosecond-precision offset from start of the trace. See {@link PendingTrace} for details. Must - * be greater than zero. - */ - private final long startTimeNano; - - /** - * The duration in nanoseconds computed using the startTimeMicro or startTimeNano. Span is - * considered finished when this is set. - */ - private final AtomicLong durationNano = new AtomicLong(); - - /** Delegates to for handling the logs if present. */ - private final LogHandler logHandler; - - /** Implementation detail. Stores the weak reference to this span. Used by TraceCollection. */ - volatile WeakReference ref; - - /** - * Spans should be constructed using the builder, not by calling the constructor directly. - * - * @param timestampMicro if greater than zero, use this time instead of the current time - * @param context the context used for the span - */ - DDSpan(final long timestampMicro, final DDSpanContext context) { - this(timestampMicro, context, new DefaultLogHandler()); - } - - /** - * Spans should be constructed using the builder, not by calling the constructor directly. - * - * @param timestampMicro if greater than zero, use this time instead of the current time - * @param context the context used for the span - * @param logHandler as the handler where to delegate the log actions - */ - DDSpan(final long timestampMicro, final DDSpanContext context, final LogHandler logHandler) { - this.context = context; - this.logHandler = logHandler; - - if (timestampMicro <= 0L) { - // record the start time - startTimeMicro = Clock.currentMicroTime(); - startTimeNano = context.getTrace().getCurrentTimeNano(); - } else { - startTimeMicro = timestampMicro; - // Timestamp have come from an external clock, so use startTimeNano as a flag - startTimeNano = 0; - } - - context.getTrace().registerSpan(this); - } - - public boolean isFinished() { - return durationNano.get() != 0; - } - - private void finishAndAddToTrace(final long durationNano) { - // ensure a min duration of 1 - if (this.durationNano.compareAndSet(0, Math.max(1, durationNano))) { - context.getTrace().addSpan(this); - } else { - } - } - - @Override - public final void finish() { - if (startTimeNano > 0) { - // no external clock was used, so we can rely on nano time - finishAndAddToTrace(context.getTrace().getCurrentTimeNano() - startTimeNano); - } else { - finish(Clock.currentMicroTime()); - } - } - - @Override - public final void finish(final long stoptimeMicros) { - finishAndAddToTrace(TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - startTimeMicro)); - } - - @Override - public DDSpan setError(final boolean error) { - context.setErrorFlag(error); - return this; - } - - /** - * Check if the span is the root parent. It means that the traceId is the same as the spanId. In - * the context of distributed tracing this will return true if an only if this is the application - * initializing the trace. - * - * @return true if root, false otherwise - */ - public final boolean isRootSpan() { - return BigInteger.ZERO.equals(context.getParentId()); - } - - @Override - @Deprecated - public MutableSpan getRootSpan() { - return getLocalRootSpan(); - } - - @Override - public MutableSpan getLocalRootSpan() { - return context().getTrace().getRootSpan(); - } - - public void setErrorMeta(final Throwable error) { - setError(true); - - setTag(DDTags.ERROR_MSG, error.getMessage()); - setTag(DDTags.ERROR_TYPE, error.getClass().getName()); - - final StringWriter errorString = new StringWriter(); - error.printStackTrace(new PrintWriter(errorString)); - setTag(DDTags.ERROR_STACK, errorString.toString()); - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#setTag(java.lang.String, java.lang.String) - */ - @Override - public final DDSpan setTag(final String tag, final String value) { - context().setTag(tag, (Object) value); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#setTag(java.lang.String, boolean) - */ - @Override - public final DDSpan setTag(final String tag, final boolean value) { - context().setTag(tag, (Object) value); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#setTag(java.lang.String, java.lang.Number) - */ - @Override - public final DDSpan setTag(final String tag, final Number value) { - context().setTag(tag, (Object) value); - return this; - } - - @Override - public Span setTag(final Tag tag, final T value) { - context().setTag(tag.getKey(), value); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#context() - */ - @Override - public final DDSpanContext context() { - return context; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#getBaggageItem(java.lang.String) - */ - @Override - public final String getBaggageItem(final String key) { - return context.getBaggageItem(key); - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#setBaggageItem(java.lang.String, java.lang.String) - */ - @Override - public final DDSpan setBaggageItem(final String key, final String value) { - context.setBaggageItem(key, value); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#setOperationName(java.lang.String) - */ - @Override - public final DDSpan setOperationName(final String operationName) { - context().setOperationName(operationName); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#log(java.util.Map) - */ - @Override - public final DDSpan log(final Map map) { - logHandler.log(map, this); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#log(long, java.util.Map) - */ - @Override - public final DDSpan log(final long l, final Map map) { - logHandler.log(l, map, this); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#log(java.lang.String) - */ - @Override - public final DDSpan log(final String s) { - logHandler.log(s, this); - return this; - } - - /* (non-Javadoc) - * @see io.opentracing.BaseSpan#log(long, java.lang.String) - */ - @Override - public final DDSpan log(final long l, final String s) { - logHandler.log(l, s, this); - return this; - } - - @Override - public final DDSpan setServiceName(final String serviceName) { - context().setServiceName(serviceName); - return this; - } - - @Override - public final DDSpan setResourceName(final String resourceName) { - context().setResourceName(resourceName); - return this; - } - - /** - * Set the sampling priority of the root span of this span's trace - * - *

Has no effect if the span priority has been propagated (injected or extracted). - */ - @Override - public final DDSpan setSamplingPriority(final int newPriority) { - context().setSamplingPriority(newPriority); - return this; - } - - @Override - public final DDSpan setSpanType(final String type) { - context().setSpanType(type); - return this; - } - - // Getters - - /** - * Meta merges baggage and tags (stringified values) - * - * @return merged context baggage and tags - */ - public Map getMeta() { - final Map meta = new HashMap<>(); - for (final Map.Entry entry : context().getBaggageItems().entrySet()) { - meta.put(entry.getKey(), entry.getValue()); - } - for (final Map.Entry entry : getTags().entrySet()) { - meta.put(entry.getKey(), String.valueOf(entry.getValue())); - } - return meta; - } - - /** - * Span metrics. - * - * @return metrics for this span - */ - public Map getMetrics() { - return context.getMetrics(); - } - - @Override - public long getStartTime() { - return startTimeNano > 0 ? startTimeNano : TimeUnit.MICROSECONDS.toNanos(startTimeMicro); - } - - @Override - public long getDurationNano() { - return durationNano.get(); - } - - @Override - public String getServiceName() { - return context.getServiceName(); - } - - public BigInteger getTraceId() { - return context.getTraceId(); - } - - public BigInteger getSpanId() { - return context.getSpanId(); - } - - public BigInteger getParentId() { - return context.getParentId(); - } - - @Override - public String getResourceName() { - return context.getResourceName(); - } - - @Override - public String getOperationName() { - return context.getOperationName(); - } - - @Override - public Integer getSamplingPriority() { - final int samplingPriority = context.getSamplingPriority(); - if (samplingPriority == PrioritySampling.UNSET) { - return null; - } else { - return samplingPriority; - } - } - - @Override - public String getSpanType() { - return context.getSpanType(); - } - - @Override - public Map getTags() { - return context().getTags(); - } - - public String getType() { - return context.getSpanType(); - } - - @Override - public Boolean isError() { - return context.getErrorFlag(); - } - - public int getError() { - return context.getErrorFlag() ? 1 : 0; - } - - @Override - public String toString() { - return new StringBuilder() - .append(context.toString()) - .append(", duration_ns=") - .append(durationNano) - .toString(); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDSpanContext.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/DDSpanContext.java deleted file mode 100644 index e63ae382c7..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDSpanContext.java +++ /dev/null @@ -1,385 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import com.datadog.opentracing.decorators.AbstractDecorator; -import com.datadog.trace.api.DDTags; -import com.datadog.trace.api.sampling.PrioritySampling; -import java.math.BigInteger; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; - -/** - * SpanContext represents Span state that must propagate to descendant Spans and across process - * boundaries. - * - *

SpanContext is logically divided into two pieces: (1) the user-level "Baggage" that propagates - * across Span boundaries and (2) any Datadog fields that are needed to identify or contextualize - * the associated Span instance - */ -public class DDSpanContext implements io.opentracing.SpanContext { - public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; - public static final String SAMPLE_RATE_KEY = "_sample_rate"; - public static final String ORIGIN_KEY = "_dd.origin"; - - private static final Map EMPTY_METRICS = Collections.emptyMap(); - - // Shared with other span contexts - /** For technical reasons, the ref to the original tracer */ - private final DDTracer tracer; - - /** The collection of all span related to this one */ - private final PendingTrace trace; - - /** Baggage is associated with the whole trace and shared with other spans */ - private final Map baggageItems; - - // Not Shared with other span contexts - private final BigInteger traceId; - private final BigInteger spanId; - private final BigInteger parentId; - - /** Tags are associated to the current span, they will not propagate to the children span */ - private final Map tags = new ConcurrentHashMap<>(); - - /** The service name is required, otherwise the span are dropped by the agent */ - private volatile String serviceName; - /** The resource associated to the service (server_web, database, etc.) */ - private volatile String resourceName; - /** Each span have an operation name describing the current span */ - private volatile String operationName; - /** The type of the span. If null, the Datadog Agent will report as a custom */ - private volatile String spanType; - /** True indicates that the span reports an error */ - private volatile boolean errorFlag; - /** - * When true, the samplingPriority cannot be changed. This prevents the sampling flag from - * changing after the context has propagated. - * - *

For thread safety, this boolean is only modified or accessed under instance lock. - */ - private boolean samplingPriorityLocked = false; - /** The origin of the trace. (eg. Synthetics) */ - private final String origin; - /** Metrics on the span */ - private final AtomicReference> metrics = new AtomicReference<>(); - - // Additional Metadata - private final String threadName = Thread.currentThread().getName(); - private final long threadId = Thread.currentThread().getId(); - - private final Map serviceNameMappings; - - public DDSpanContext( - final BigInteger traceId, - final BigInteger spanId, - final BigInteger parentId, - final String serviceName, - final String operationName, - final String resourceName, - final int samplingPriority, - final String origin, - final Map baggageItems, - final boolean errorFlag, - final String spanType, - final Map tags, - final PendingTrace trace, - final DDTracer tracer, - final Map serviceNameMappings) { - - assert tracer != null; - assert trace != null; - this.tracer = tracer; - this.trace = trace; - - assert traceId != null; - assert spanId != null; - assert parentId != null; - this.traceId = traceId; - this.spanId = spanId; - this.parentId = parentId; - - if (baggageItems == null) { - this.baggageItems = new ConcurrentHashMap<>(0); - } else { - this.baggageItems = new ConcurrentHashMap<>(baggageItems); - } - - if (tags != null) { - this.tags.putAll(tags); - } - - this.serviceNameMappings = serviceNameMappings; - setServiceName(serviceName); - this.operationName = operationName; - this.resourceName = resourceName; - this.errorFlag = errorFlag; - this.spanType = spanType; - this.origin = origin; - - if (samplingPriority != PrioritySampling.UNSET) { - setSamplingPriority(samplingPriority); - } - - if (origin != null) { - this.tags.put(ORIGIN_KEY, origin); - } - this.tags.put(DDTags.THREAD_NAME, threadName); - this.tags.put(DDTags.THREAD_ID, threadId); - } - - public BigInteger getTraceId() { - return traceId; - } - - @Override - public String toTraceId() { - return traceId.toString(); - } - - public BigInteger getParentId() { - return parentId; - } - - public BigInteger getSpanId() { - return spanId; - } - - @Override - public String toSpanId() { - return spanId.toString(); - } - - public String getServiceName() { - return serviceName; - } - - public void setServiceName(final String serviceName) { - if (serviceNameMappings.containsKey(serviceName)) { - this.serviceName = serviceNameMappings.get(serviceName); - } else { - this.serviceName = serviceName; - } - } - - public String getResourceName() { - return isResourceNameSet() ? resourceName : operationName; - } - - public boolean isResourceNameSet() { - return !(resourceName == null || resourceName.isEmpty()); - } - - public boolean hasResourceName() { - return isResourceNameSet() || tags.containsKey(DDTags.RESOURCE_NAME); - } - - public void setResourceName(final String resourceName) { - this.resourceName = resourceName; - } - - public String getOperationName() { - return operationName; - } - - public void setOperationName(final String operationName) { - this.operationName = operationName; - } - - public boolean getErrorFlag() { - return errorFlag; - } - - public void setErrorFlag(final boolean errorFlag) { - this.errorFlag = errorFlag; - } - - public String getSpanType() { - return spanType; - } - - public void setSpanType(final String spanType) { - this.spanType = spanType; - } - - /** @return if sampling priority was set by this method invocation */ - public boolean setSamplingPriority(final int newPriority) { - if (newPriority == PrioritySampling.UNSET) { - return false; - } - - if (trace != null) { - final DDSpan rootSpan = trace.getRootSpan(); - if (null != rootSpan && rootSpan.context() != this) { - return rootSpan.context().setSamplingPriority(newPriority); - } - } - - // sync with lockSamplingPriority - synchronized (this) { - if (samplingPriorityLocked) { - return false; - } else { - setMetric(PRIORITY_SAMPLING_KEY, newPriority); - return true; - } - } - } - - /** @return the sampling priority of this span's trace, or null if no priority has been set */ - public int getSamplingPriority() { - final DDSpan rootSpan = trace.getRootSpan(); - if (null != rootSpan && rootSpan.context() != this) { - return rootSpan.context().getSamplingPriority(); - } - - final Number val = getMetrics().get(PRIORITY_SAMPLING_KEY); - return null == val ? PrioritySampling.UNSET : val.intValue(); - } - - /** - * Prevent future changes to the context's sampling priority. - * - *

Used when a span is extracted or injected for propagation. - * - *

Has no effect if the sampling priority is unset. - * - * @return true if the sampling priority was locked. - */ - public boolean lockSamplingPriority() { - final DDSpan rootSpan = trace.getRootSpan(); - if (null != rootSpan && rootSpan.context() != this) { - return rootSpan.context().lockSamplingPriority(); - } - - // sync with setSamplingPriority - synchronized (this) { - if (getMetrics().get(PRIORITY_SAMPLING_KEY) == null) { - } else if (samplingPriorityLocked == false) { - samplingPriorityLocked = true; - } - return samplingPriorityLocked; - } - } - - public String getOrigin() { - final DDSpan rootSpan = trace.getRootSpan(); - if (null != rootSpan) { - return rootSpan.context().origin; - } else { - return origin; - } - } - - public void setBaggageItem(final String key, final String value) { - baggageItems.put(key, value); - } - - public String getBaggageItem(final String key) { - return baggageItems.get(key); - } - - public Map getBaggageItems() { - return baggageItems; - } - - /* (non-Javadoc) - * @see io.opentracing.SpanContext#baggageItems() - */ - @Override - public Iterable> baggageItems() { - return baggageItems.entrySet(); - } - - public PendingTrace getTrace() { - return trace; - } - - @Deprecated - public DDTracer getTracer() { - return tracer; - } - - public Map getMetrics() { - final Map metrics = this.metrics.get(); - return metrics == null ? EMPTY_METRICS : metrics; - } - - public void setMetric(final String key, final Number value) { - if (metrics.get() == null) { - metrics.compareAndSet(null, new ConcurrentHashMap()); - } - if (value instanceof Float) { - metrics.get().put(key, value.doubleValue()); - } else { - metrics.get().put(key, value); - } - } - /** - * Add a tag to the span. Tags are not propagated to the children - * - * @param tag the tag-name - * @param value the value of the tag. tags with null values are ignored. - */ - public synchronized void setTag(final String tag, final Object value) { - if (value == null || (value instanceof String && ((String) value).isEmpty())) { - tags.remove(tag); - return; - } - - boolean addTag = true; - - // Call decorators - final List decorators = tracer.getSpanContextDecorators(tag); - if (decorators != null) { - for (final AbstractDecorator decorator : decorators) { - try { - addTag &= decorator.shouldSetTag(this, tag, value); - } catch (final Throwable ex) { - } - } - } - - if (addTag) { - tags.put(tag, value); - } - } - - public synchronized Map getTags() { - return Collections.unmodifiableMap(tags); - } - - @Override - public String toString() { - final StringBuilder s = - new StringBuilder() - .append("DDSpan [ t_id=") - .append(traceId) - .append(", s_id=") - .append(spanId) - .append(", p_id=") - .append(parentId) - .append("] trace=") - .append(getServiceName()) - .append("/") - .append(getOperationName()) - .append("/") - .append(getResourceName()) - .append(" metrics=") - .append(new TreeMap<>(getMetrics())); - if (errorFlag) { - s.append(" *errored*"); - } - - s.append(" tags=").append(new TreeMap<>(tags)); - return s.toString(); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDTraceOTInfo.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/DDTraceOTInfo.java deleted file mode 100644 index f0abefb8e0..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDTraceOTInfo.java +++ /dev/null @@ -1,40 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import java.io.BufferedReader; -import java.io.InputStreamReader; - -public class DDTraceOTInfo { - - public static final String JAVA_VERSION = System.getProperty("java.version", "unknown"); - public static final String JAVA_VM_NAME = System.getProperty("java.vm.name", "unknown"); - public static final String JAVA_VM_VENDOR = System.getProperty("java.vm.vendor", "unknown"); - - public static final String VERSION; - - static { - String v; - try (final BufferedReader br = - new BufferedReader( - new InputStreamReader( - DDTraceOTInfo.class.getResourceAsStream("/dd-trace-ot.version"), "UTF-8"))) { - final StringBuilder sb = new StringBuilder(); - - for (int c = br.read(); c != -1; c = br.read()) sb.append((char) c); - - v = sb.toString().trim(); - } catch (final Exception e) { - v = "unknown"; - } - VERSION = v; - } - - public static void main(final String... args) { - System.out.println(VERSION); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDTracer.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/DDTracer.java deleted file mode 100644 index c4ad56fbd6..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/DDTracer.java +++ /dev/null @@ -1,730 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import com.datadog.opentracing.decorators.AbstractDecorator; -import com.datadog.opentracing.decorators.DDDecoratorsFactory; -import com.datadog.opentracing.jfr.DDNoopScopeEventFactory; -import com.datadog.opentracing.jfr.DDScopeEventFactory; -import com.datadog.opentracing.propagation.ExtractedContext; -import com.datadog.opentracing.propagation.HttpCodec; -import com.datadog.opentracing.propagation.TagContext; -import com.datadog.opentracing.scopemanager.ContextualScopeManager; -import com.datadog.opentracing.scopemanager.ScopeContext; -import com.datadog.trace.api.Config; -import com.datadog.trace.api.Tracer; -import com.datadog.trace.api.interceptor.MutableSpan; -import com.datadog.trace.api.interceptor.TraceInterceptor; -import com.datadog.trace.api.sampling.PrioritySampling; -import com.datadog.trace.common.sampling.PrioritySampler; -import com.datadog.trace.common.sampling.Sampler; -import com.datadog.trace.common.writer.LoggingWriter; -import com.datadog.trace.common.writer.Writer; -import com.datadog.trace.context.ScopeListener; -import io.opentracing.References; -import io.opentracing.Scope; -import io.opentracing.ScopeManager; -import io.opentracing.Span; -import io.opentracing.SpanContext; -import io.opentracing.propagation.Format; -import io.opentracing.propagation.TextMapExtract; -import io.opentracing.propagation.TextMapInject; -import io.opentracing.tag.Tag; - -import java.io.Closeable; -import java.lang.ref.WeakReference; -import java.math.BigInteger; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListSet; - -/** - * DDTracer makes it easy to send traces and span to DD using the OpenTracing API. - */ -public class DDTracer implements io.opentracing.Tracer, Closeable, Tracer { - // UINT64 max value - public static final BigInteger TRACE_ID_MAX = - BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE); - public static final BigInteger TRACE_ID_MIN = BigInteger.ZERO; - - /** - * Default service name if none provided on the trace or span - */ - final String serviceName; - /** - * Writer is an charge of reporting traces and spans to the desired endpoint - */ - final Writer writer; - /** - * Sampler defines the sampling policy in order to reduce the number of traces for instance - */ - final Sampler sampler; - /** - * Scope manager is in charge of managing the scopes from which spans are created - */ - final ScopeManager scopeManager; - - /** - * A set of tags that are added only to the application's root span - */ - private final Map localRootSpanTags; - /** - * A set of tags that are added to every span - */ - private final Map defaultSpanTags; - /** - * A configured mapping of service names to update with new values - */ - private final Map serviceNameMappings; - - /** - * number of spans in a pending trace before they get flushed - */ - private final int partialFlushMinSpans; - - /** - * JVM shutdown callback, keeping a reference to it to remove this if DDTracer gets destroyed - * earlier - */ - private final Thread shutdownCallback; - - /** - * Span context decorators - */ - private final Map> spanContextDecorators = - new ConcurrentHashMap<>(); - - private final SortedSet interceptors = - new ConcurrentSkipListSet<>( - new Comparator() { - @Override - public int compare(final TraceInterceptor o1, final TraceInterceptor o2) { - return Integer.compare(o1.priority(), o2.priority()); - } - }); - - private final HttpCodec.Injector injector; - private final HttpCodec.Extractor extractor; - - // On Android, the same zygote is reused for every single application, - // meaning that the ThreadLocalRandom reuses the same exact state, - // resulting in conflicting TraceIds. - // To avoid this we will use a SecureRandom instance here to generate the trace id. - - private final Random random; - - protected DDTracer(final Config config, final Writer writer, final Random random) { - this( - config.getServiceName(), - writer, - Sampler.Builder.forConfig(config), - HttpCodec.createInjector(Config.get()), - HttpCodec.createExtractor(Config.get(), config.getHeaderTags()), - new ContextualScopeManager(Config.get().getScopeDepthLimit(), createScopeEventFactory()), - random, - config.getLocalRootSpanTags(), - config.getMergedSpanTags(), - config.getServiceMapping(), - config.getHeaderTags(), - config.getPartialFlushMinSpans()); - } - - - // These field names must be stable to ensure the builder api is stable. - private DDTracer( - final String serviceName, - final Writer writer, - final Sampler sampler, - final HttpCodec.Injector injector, - final HttpCodec.Extractor extractor, - final ScopeManager scopeManager, - final Random random, - final Map localRootSpanTags, - final Map defaultSpanTags, - final Map serviceNameMappings, - final Map taggedHeaders, - final int partialFlushMinSpans) { - - assert localRootSpanTags != null; - assert defaultSpanTags != null; - assert serviceNameMappings != null; - assert taggedHeaders != null; - - this.random = random; - this.serviceName = serviceName; - if (writer == null) { - this.writer = new LoggingWriter(); - } else { - this.writer = writer; - } - this.sampler = sampler; - this.injector = injector; - this.extractor = extractor; - this.scopeManager = scopeManager; - this.localRootSpanTags = localRootSpanTags; - this.defaultSpanTags = defaultSpanTags; - this.serviceNameMappings = serviceNameMappings; - this.partialFlushMinSpans = partialFlushMinSpans; - - this.writer.start(); - - shutdownCallback = new ShutdownHook(this); - try { - Runtime.getRuntime().addShutdownHook(shutdownCallback); - } catch (final IllegalStateException ex) { - // The JVM is already shutting down. - } - - final List decorators = DDDecoratorsFactory.createBuiltinDecorators(); - for (final AbstractDecorator decorator : decorators) { - addDecorator(decorator); - } - - registerClassLoader(ClassLoader.getSystemClassLoader()); - - // Ensure that PendingTrace.SPAN_CLEANER is initialized in this thread: - // FIXME: add test to verify the span cleaner thread is started with this call. - PendingTrace.initialize(); - } - - @Override - public void finalize() { - try { - Runtime.getRuntime().removeShutdownHook(shutdownCallback); - shutdownCallback.run(); - } catch (final Exception e) { - } - } - - /** - * Returns the list of span context decorators - * - * @return the list of span context decorators - */ - public List getSpanContextDecorators(final String tag) { - return spanContextDecorators.get(tag); - } - - /** - * Add a new decorator in the list ({@link AbstractDecorator}) - * - * @param decorator The decorator in the list - */ - public void addDecorator(final AbstractDecorator decorator) { - - List list = spanContextDecorators.get(decorator.getMatchingTag()); - if (list == null) { - list = new ArrayList<>(); - } - list.add(decorator); - - spanContextDecorators.put(decorator.getMatchingTag(), list); - } - - @Deprecated - public void addScopeContext(final ScopeContext context) { - if (scopeManager instanceof ContextualScopeManager) { - ((ContextualScopeManager) scopeManager).addScopeContext(context); - } - } - - /** - * If an application is using a non-system classloader, that classloader should be registered - * here. Due to the way Spring Boot structures its' executable jar, this might log some warnings. - * - * @param classLoader to register. - */ - public void registerClassLoader(final ClassLoader classLoader) { - try { - for (final TraceInterceptor interceptor : - ServiceLoader.load(TraceInterceptor.class, classLoader)) { - addTraceInterceptor(interceptor); - } - } catch (final ServiceConfigurationError e) { - } - } - - @Override - public ScopeManager scopeManager() { - return scopeManager; - } - - @Override - public Span activeSpan() { - return scopeManager.activeSpan(); - } - - @Override - public Scope activateSpan(final Span span) { - return scopeManager.activate(span); - } - - @Override - public SpanBuilder buildSpan(final String operationName) { - return new DDSpanBuilder(operationName, scopeManager); - } - - @Override - public void inject(final SpanContext spanContext, final Format format, final T carrier) { - if (carrier instanceof TextMapInject) { - final DDSpanContext ddSpanContext = (DDSpanContext) spanContext; - - final DDSpan rootSpan = ddSpanContext.getTrace().getRootSpan(); - setSamplingPriorityIfNecessary(rootSpan); - - injector.inject(ddSpanContext, (TextMapInject) carrier); - } else { - } - } - - @Override - public SpanContext extract(final Format format, final T carrier) { - if (carrier instanceof TextMapExtract) { - return extractor.extract((TextMapExtract) carrier); - } else { - return null; - } - } - - /** - * We use the sampler to know if the trace has to be reported/written. The sampler is called on - * the first span (root span) of the trace. If the trace is marked as a sample, we report it. - * - * @param trace a list of the spans related to the same trace - */ - void write(final Collection trace) { - if (trace.isEmpty()) { - return; - } - final ArrayList writtenTrace; - if (interceptors.isEmpty()) { - writtenTrace = new ArrayList<>(trace); - } else { - Collection interceptedTrace = new ArrayList<>(trace); - for (final TraceInterceptor interceptor : interceptors) { - interceptedTrace = interceptor.onTraceComplete(interceptedTrace); - } - writtenTrace = new ArrayList<>(interceptedTrace.size()); - for (final MutableSpan span : interceptedTrace) { - if (span instanceof DDSpan) { - writtenTrace.add((DDSpan) span); - } - } - } - incrementTraceCount(); - - if (!writtenTrace.isEmpty()) { - final DDSpan rootSpan = (DDSpan) writtenTrace.get(0).getLocalRootSpan(); - setSamplingPriorityIfNecessary(rootSpan); - - final DDSpan spanToSample = rootSpan == null ? writtenTrace.get(0) : rootSpan; - if (sampler.sample(spanToSample)) { - writer.write(writtenTrace); - } - } - } - - void setSamplingPriorityIfNecessary(final DDSpan rootSpan) { - // There's a race where multiple threads can see PrioritySampling.UNSET here - // This check skips potential complex sampling priority logic when we know its redundant - // Locks inside DDSpanContext ensure the correct behavior in the race case - - if (sampler instanceof PrioritySampler - && rootSpan != null - && rootSpan.context().getSamplingPriority() == PrioritySampling.UNSET) { - - ((PrioritySampler) sampler).setSamplingPriority(rootSpan); - } - } - - /** - * Increment the reported trace count, but do not write a trace. - */ - void incrementTraceCount() { - writer.incrementTraceCount(); - } - - @Override - public String getTraceId() { - final Span activeSpan = activeSpan(); - if (activeSpan instanceof DDSpan) { - return ((DDSpan) activeSpan).getTraceId().toString(); - } - return "0"; - } - - @Override - public String getSpanId() { - final Span activeSpan = activeSpan(); - if (activeSpan instanceof DDSpan) { - return ((DDSpan) activeSpan).getSpanId().toString(); - } - return "0"; - } - - @Override - public boolean addTraceInterceptor(final TraceInterceptor interceptor) { - return interceptors.add(interceptor); - } - - @Override - public void addScopeListener(final ScopeListener listener) { - if (scopeManager instanceof ContextualScopeManager) { - ((ContextualScopeManager) scopeManager).addScopeListener(listener); - } - } - - @Override - public void close() { - PendingTrace.close(); - writer.close(); - } - - @Override - public String toString() { - return "DDTracer-" - + Integer.toHexString(hashCode()) - + "{ serviceName=" - + serviceName - + ", writer=" - + writer - + ", sampler=" - + sampler - + ", defaultSpanTags=" - + defaultSpanTags - + '}'; - } - - @Deprecated - private static Map customRuntimeTags( - final String runtimeId, final Map applicationRootSpanTags) { - final Map runtimeTags = new HashMap<>(applicationRootSpanTags); - runtimeTags.put(Config.RUNTIME_ID_TAG, runtimeId); - return Collections.unmodifiableMap(runtimeTags); - } - - private static DDScopeEventFactory createScopeEventFactory() { - try { - return (DDScopeEventFactory) - Class.forName("com.datadog.opentracing.jfr.openjdk.ScopeEventFactory").newInstance(); - } catch (final ClassFormatError | ReflectiveOperationException | NoClassDefFoundError e) { - } - return new DDNoopScopeEventFactory(); - } - - /** - * Spans are built using this builder - */ - public class DDSpanBuilder implements SpanBuilder { - private final ScopeManager scopeManager; - - /** - * Each span must have an operationName according to the opentracing specification - */ - private final String operationName; - - // Builder attributes - private final Map tags = new LinkedHashMap(defaultSpanTags); - private long timestampMicro; - private SpanContext parent; - private String serviceName; - private String resourceName; - private boolean errorFlag; - private String spanType; - private boolean ignoreScope = false; - private LogHandler logHandler = new DefaultLogHandler(); - - public DDSpanBuilder(final String operationName, final ScopeManager scopeManager) { - this.operationName = operationName; - this.scopeManager = scopeManager; - } - - @Override - public SpanBuilder ignoreActiveSpan() { - ignoreScope = true; - return this; - } - - private Span startSpan() { - return new DDSpan(timestampMicro, buildSpanContext(), logHandler); - } - - @Override - public Scope startActive(final boolean finishSpanOnClose) { - final Span span = startSpan(); - final Scope scope = scopeManager.activate(span, finishSpanOnClose); - return scope; - } - - @Override - @Deprecated - public Span startManual() { - return start(); - } - - @Override - public Span start() { - final Span span = startSpan(); - return span; - } - - @Override - public DDSpanBuilder withTag(final String tag, final Number number) { - return withTag(tag, (Object) number); - } - - @Override - public DDSpanBuilder withTag(final String tag, final String string) { - return withTag(tag, (Object) string); - } - - @Override - public DDSpanBuilder withTag(final String tag, final boolean bool) { - return withTag(tag, (Object) bool); - } - - @Override - public SpanBuilder withTag(final Tag tag, final T value) { - return withTag(tag.getKey(), value); - } - - @Override - public DDSpanBuilder withStartTimestamp(final long timestampMicroseconds) { - timestampMicro = timestampMicroseconds; - return this; - } - - public DDSpanBuilder withServiceName(final String serviceName) { - this.serviceName = serviceName; - return this; - } - - public DDSpanBuilder withResourceName(final String resourceName) { - this.resourceName = resourceName; - return this; - } - - public DDSpanBuilder withErrorFlag() { - errorFlag = true; - return this; - } - - public DDSpanBuilder withSpanType(final String spanType) { - this.spanType = spanType; - return this; - } - - public Iterable> baggageItems() { - if (parent == null) { - return Collections.emptyList(); - } - return parent.baggageItems(); - } - - public DDSpanBuilder withLogHandler(final LogHandler logHandler) { - if (logHandler != null) { - this.logHandler = logHandler; - } - return this; - } - - @Override - public DDSpanBuilder asChildOf(final Span span) { - return asChildOf(span == null ? null : span.context()); - } - - @Override - public DDSpanBuilder asChildOf(final SpanContext spanContext) { - parent = spanContext; - return this; - } - - @Override - public DDSpanBuilder addReference(final String referenceType, final SpanContext spanContext) { - if (spanContext == null) { - return this; - } - if (!(spanContext instanceof ExtractedContext) && !(spanContext instanceof DDSpanContext)) { - return this; - } - if (References.CHILD_OF.equals(referenceType) - || References.FOLLOWS_FROM.equals(referenceType)) { - return asChildOf(spanContext); - } else { - } - return this; - } - - // Private methods - private DDSpanBuilder withTag(final String tag, final Object value) { - if (value == null || (value instanceof String && ((String) value).isEmpty())) { - tags.remove(tag); - } else { - tags.put(tag, value); - } - return this; - } - - private BigInteger generateNewId() { - // It is **extremely** unlikely to generate the value "0" but we still need to handle that - // case - BigInteger value; - do { - synchronized (random) { - value = new StringCachingBigInteger(63, random); - } - } while (value.signum() == 0); - - return value; - } - - /** - * Build the SpanContext, if the actual span has a parent, the following attributes must be - * propagated: - ServiceName - Baggage - Trace (a list of all spans related) - SpanType - * - * @return the context - */ - private DDSpanContext buildSpanContext() { - final BigInteger traceId; - final BigInteger spanId = generateNewId(); - final BigInteger parentSpanId; - final Map baggage; - final PendingTrace parentTrace; - final int samplingPriority; - final String origin; - - final DDSpanContext context; - SpanContext parentContext = parent; - if (parentContext == null && !ignoreScope) { - // use the Scope as parent unless overridden or ignored. - final Span activeSpan = scopeManager.activeSpan(); - if (activeSpan != null) { - parentContext = activeSpan.context(); - } - } - - // Propagate internal trace. - // Note: if we are not in the context of distributed tracing and we are starting the first - // root span, parentContext will be null at this point. - if (parentContext instanceof DDSpanContext) { - final DDSpanContext ddsc = (DDSpanContext) parentContext; - traceId = ddsc.getTraceId(); - parentSpanId = ddsc.getSpanId(); - baggage = ddsc.getBaggageItems(); - parentTrace = ddsc.getTrace(); - samplingPriority = PrioritySampling.UNSET; - origin = null; - if (serviceName == null) { - serviceName = ddsc.getServiceName(); - } - - } else { - if (parentContext instanceof ExtractedContext) { - // Propagate external trace - final ExtractedContext extractedContext = (ExtractedContext) parentContext; - traceId = extractedContext.getTraceId(); - parentSpanId = extractedContext.getSpanId(); - samplingPriority = extractedContext.getSamplingPriority(); - baggage = extractedContext.getBaggage(); - } else { - // Start a new trace - traceId = generateNewId(); - parentSpanId = BigInteger.ZERO; - samplingPriority = PrioritySampling.UNSET; - baggage = null; - } - - // Get header tags and set origin whether propagating or not. - if (parentContext instanceof TagContext) { - tags.putAll(((TagContext) parentContext).getTags()); - origin = ((TagContext) parentContext).getOrigin(); - } else { - origin = null; - } - - tags.putAll(localRootSpanTags); - - parentTrace = new PendingTrace(DDTracer.this, traceId); - } - - if (serviceName == null) { - serviceName = DDTracer.this.serviceName; - } - - final String operationName = this.operationName != null ? this.operationName : resourceName; - - // some attributes are inherited from the parent - context = - new DDSpanContext( - traceId, - spanId, - parentSpanId, - serviceName, - operationName, - resourceName, - samplingPriority, - origin, - baggage, - errorFlag, - spanType, - tags, - parentTrace, - DDTracer.this, - serviceNameMappings); - - // Apply Decorators to handle any tags that may have been set via the builder. - for (final Map.Entry tag : tags.entrySet()) { - if (tag.getValue() == null) { - context.setTag(tag.getKey(), null); - continue; - } - - boolean addTag = true; - - // Call decorators - final List decorators = getSpanContextDecorators(tag.getKey()); - if (decorators != null) { - for (final AbstractDecorator decorator : decorators) { - try { - addTag &= decorator.shouldSetTag(context, tag.getKey(), tag.getValue()); - } catch (final Throwable ex) { - } - } - } - - if (!addTag) { - context.setTag(tag.getKey(), null); - } - } - - return context; - } - } - - private static class ShutdownHook extends Thread { - private final WeakReference reference; - - private ShutdownHook(final DDTracer tracer) { - super("dd-tracer-shutdown-hook"); - reference = new WeakReference<>(tracer); - } - - @Override - public void run() { - final DDTracer tracer = reference.get(); - if (tracer != null) { - tracer.close(); - } - } - } - - // GENERATED GETTER - - public int getPartialFlushMinSpans() { - return partialFlushMinSpans; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/DefaultLogHandler.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/DefaultLogHandler.java deleted file mode 100644 index 0222ecd329..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/DefaultLogHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import static io.opentracing.log.Fields.ERROR_OBJECT; -import static io.opentracing.log.Fields.MESSAGE; - -import com.datadog.trace.api.DDTags; -import java.util.Map; - -/** The default implementation of the LogHandler. */ -public class DefaultLogHandler implements LogHandler { - @Override - public void log(Map fields, DDSpan span) { - extractError(fields, span); - } - - @Override - public void log(long timestampMicroseconds, Map fields, DDSpan span) { - extractError(fields, span); - } - - @Override - public void log(String event, DDSpan span) { - } - - @Override - public void log(long timestampMicroseconds, String event, DDSpan span) { - } - - private void extractError(final Map map, DDSpan span) { - if (map.get(ERROR_OBJECT) instanceof Throwable) { - final Throwable error = (Throwable) map.get(ERROR_OBJECT); - span.setErrorMeta(error); - } else if (map.get(MESSAGE) instanceof String) { - span.setTag(DDTags.ERROR_MSG, (String) map.get(MESSAGE)); - } - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/LogHandler.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/LogHandler.java deleted file mode 100644 index f44e13de3f..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/LogHandler.java +++ /dev/null @@ -1,49 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import java.util.Map; - -public interface LogHandler { - - /** - * Handles the log implementation in the Span. - * - * @param fields key:value log fields. Tracer implementations should support String, numeric, and - * boolean values; some may also support arbitrary Objects. - * @param span from which the call was made - */ - void log(Map fields, DDSpan span); - - /** - * Handles the log implementation in the Span. - * - * @param timestampMicroseconds The explicit timestamp for the log record. Must be greater than or - * equal to the Span's start timestamp. - * @param fields key:value log fields. Tracer implementations should support String, numeric, and - * @param span from which the call was made - */ - void log(long timestampMicroseconds, Map fields, DDSpan span); - - /** - * Handles the log implementation in the Span.. - * - * @param event the event value; often a stable identifier for a moment in the Span lifecycle - * @param span from which the call was made - */ - void log(String event, DDSpan span); - - /** - * Handles the log implementation in the Span. - * - * @param timestampMicroseconds The explicit timestamp for the log record. Must be greater than or - * equal to the Span's start timestamp. - * @param event the event value; often a stable identifier for a moment in the Span lifecycle - * @param span from which the call was made - */ - void log(long timestampMicroseconds, String event, DDSpan span); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/PendingTrace.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/PendingTrace.java deleted file mode 100644 index 6bd76a5edf..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/PendingTrace.java +++ /dev/null @@ -1,310 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import com.datadog.exec.CommonTaskExecutor; -import com.datadog.exec.CommonTaskExecutor.Task; -import com.datadog.opentracing.scopemanager.ContinuableScope; -import com.datadog.trace.common.util.Clock; -import java.io.Closeable; -import java.lang.ref.Reference; -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; -import java.math.BigInteger; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -public class PendingTrace extends LinkedList { - private static final AtomicReference SPAN_CLEANER = new AtomicReference<>(); - - private final DDTracer tracer; - private final BigInteger traceId; - - // TODO: consider moving these time fields into DDTracer to ensure that traces have precise - // relative time - /** Trace start time in nano seconds measured up to a millisecond accuracy */ - private final long startTimeNano; - /** Nano second ticks value at trace start */ - private final long startNanoTicks; - - private final ReferenceQueue referenceQueue = new ReferenceQueue(); - private final Set> weakReferences = - Collections.newSetFromMap(new ConcurrentHashMap, Boolean>()); - - private final AtomicInteger pendingReferenceCount = new AtomicInteger(0); - - // We must maintain a separate count because ConcurrentLinkedDeque.size() is a linear operation. - private final AtomicInteger completedSpanCount = new AtomicInteger(0); - /** - * During a trace there are cases where the root span must be accessed (e.g. priority sampling and - * trace-search tags). - * - *

Use a weak ref because we still need to handle buggy cases where the root span is not - * correctly closed (see SpanCleaner). - * - *

The root span will be available in non-buggy cases because it has either finished and - * strongly ref'd in this queue or is unfinished and ref'd in a ContinuableScope. - */ - private final AtomicReference> rootSpan = new AtomicReference<>(); - - /** Ensure a trace is never written multiple times */ - private final AtomicBoolean isWritten = new AtomicBoolean(false); - - PendingTrace(final DDTracer tracer, final BigInteger traceId) { - this.tracer = tracer; - this.traceId = traceId; - - startTimeNano = Clock.currentNanoTime(); - startNanoTicks = Clock.currentNanoTicks(); - - addPendingTrace(); - } - - /** - * Current timestamp in nanoseconds. - * - *

Note: it is not possible to get 'real' nanosecond time. This method uses trace start time - * (which has millisecond precision) as a reference and it gets time with nanosecond precision - * after that. This means time measured within same Trace in different Spans is relatively correct - * with nanosecond precision. - * - * @return timestamp in nanoseconds - */ - public long getCurrentTimeNano() { - return startTimeNano + Math.max(0, Clock.currentNanoTicks() - startNanoTicks); - } - - public void registerSpan(final DDSpan span) { - if (traceId == null || span.context() == null) { - return; - } - if (!traceId.equals(span.context().getTraceId())) { - return; - } - rootSpan.compareAndSet(null, new WeakReference<>(span)); - synchronized (span) { - if (null == span.ref) { - span.ref = new WeakReference(span, referenceQueue); - weakReferences.add(span.ref); - final int count = pendingReferenceCount.incrementAndGet(); - } else { - } - } - } - - private void expireSpan(final DDSpan span) { - if (traceId == null || span.context() == null) { - return; - } - if (!traceId.equals(span.context().getTraceId())) { - return; - } - synchronized (span) { - if (null == span.ref) { - } else { - weakReferences.remove(span.ref); - span.ref.clear(); - span.ref = null; - expireReference(); - } - } - } - - public void addSpan(final DDSpan span) { - if (span.getDurationNano() == 0) { - return; - } - if (traceId == null || span.context() == null) { - return; - } - if (!traceId.equals(span.getTraceId())) { - return; - } - - if (!isWritten.get()) { - addFirst(span); - } else { - } - expireSpan(span); - } - - public DDSpan getRootSpan() { - final WeakReference rootRef = rootSpan.get(); - return rootRef == null ? null : rootRef.get(); - } - - /** - * When using continuations, it's possible one may be used after all existing spans are otherwise - * completed, so we need to wait till continuations are de-referenced before reporting. - */ - public void registerContinuation(final ContinuableScope.Continuation continuation) { - synchronized (continuation) { - if (continuation.ref == null) { - continuation.ref = - new WeakReference(continuation, referenceQueue); - weakReferences.add(continuation.ref); - final int count = pendingReferenceCount.incrementAndGet(); - } else { - } - } - } - - public void cancelContinuation(final ContinuableScope.Continuation continuation) { - synchronized (continuation) { - if (continuation.ref == null) { - } else { - weakReferences.remove(continuation.ref); - continuation.ref.clear(); - continuation.ref = null; - expireReference(); - } - } - } - - private void expireReference() { - final int count = pendingReferenceCount.decrementAndGet(); - if (count == 0) { - write(); - } else { - if (tracer.getPartialFlushMinSpans() > 0 && size() > tracer.getPartialFlushMinSpans()) { - synchronized (this) { - if (size() > tracer.getPartialFlushMinSpans()) { - final DDSpan rootSpan = getRootSpan(); - final List partialTrace = new ArrayList(size()); - final Iterator it = iterator(); - while (it.hasNext()) { - final DDSpan span = it.next(); - if (span != rootSpan) { - partialTrace.add(span); - completedSpanCount.decrementAndGet(); - it.remove(); - } - } - tracer.write(partialTrace); - } - } - } - } - } - - private synchronized void write() { - if (isWritten.compareAndSet(false, true)) { - removePendingTrace(); - if (!isEmpty()) { - tracer.write(this); - } - } - } - - public synchronized boolean clean() { - Reference ref; - int count = 0; - while ((ref = referenceQueue.poll()) != null) { - weakReferences.remove(ref); - if (isWritten.compareAndSet(false, true)) { - removePendingTrace(); - // preserve throughput count. - // Don't report the trace because the data comes from buggy uses of the api and is suspect. - tracer.incrementTraceCount(); - } - count++; - expireReference(); - } - if (count > 0) { - // TODO attempt to flatten and report if top level spans are finished. (for accurate metrics) - } - return count > 0; - } - - @Override - public void addFirst(final DDSpan span) { - super.addFirst(span); - completedSpanCount.incrementAndGet(); - } - - @Override - public int size() { - return completedSpanCount.get(); - } - - private void addPendingTrace() { - final SpanCleaner cleaner = SPAN_CLEANER.get(); - if (cleaner != null) { - cleaner.pendingTraces.add(this); - } - } - - private void removePendingTrace() { - final SpanCleaner cleaner = SPAN_CLEANER.get(); - if (cleaner != null) { - cleaner.pendingTraces.remove(this); - } - } - - static void initialize() { - final SpanCleaner oldCleaner = SPAN_CLEANER.getAndSet(new SpanCleaner()); - if (oldCleaner != null) { - oldCleaner.close(); - } - } - - static void close() { - final SpanCleaner cleaner = SPAN_CLEANER.getAndSet(null); - if (cleaner != null) { - cleaner.close(); - } - } - - // FIXME: it should be possible to simplify this logic and avoid having SpanCleaner and - // SpanCleanerTask - private static class SpanCleaner implements Runnable, Closeable { - private static final long CLEAN_FREQUENCY = 1; - - private final Set pendingTraces = - Collections.newSetFromMap(new ConcurrentHashMap()); - - public SpanCleaner() { - CommonTaskExecutor.INSTANCE.scheduleAtFixedRate( - SpanCleanerTask.INSTANCE, - this, - 0, - CLEAN_FREQUENCY, - TimeUnit.SECONDS, - "Pending trace cleaner"); - } - - @Override - public void run() { - for (final PendingTrace trace : pendingTraces) { - trace.clean(); - } - } - - @Override - public void close() { - // Make sure that whatever was left over gets cleaned up - run(); - } - } - - /* - * Important to use explicit class to avoid implicit hard references to cleaners from within executor. - */ - private static class SpanCleanerTask implements Task { - - static final SpanCleanerTask INSTANCE = new SpanCleanerTask(); - - @Override - public void run(final SpanCleaner target) { - target.run(); - } - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/StringCachingBigInteger.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/StringCachingBigInteger.java deleted file mode 100644 index a56f6a983c..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/StringCachingBigInteger.java +++ /dev/null @@ -1,62 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing; - -import java.math.BigInteger; -import java.util.Random; - -/** - * Because we are using BigInteger for Trace and Span Id, the toString() operator may result in - * heavy computation and string allocation overhead. In order to limit this, we are caching the - * result of toString, thereby taking advantage of the immutability of BigInteger. - */ -public class StringCachingBigInteger extends BigInteger { - - private String cachedString; - - public StringCachingBigInteger(byte[] val) { - super(val); - } - - public StringCachingBigInteger(int signum, byte[] magnitude) { - super(signum, magnitude); - } - - public StringCachingBigInteger(String val, int radix) { - super(val, radix); - } - - public StringCachingBigInteger(String val) { - super(val); - } - - public StringCachingBigInteger(int numBits, Random rnd) { - super(numBits, rnd); - } - - public StringCachingBigInteger(int bitLength, int certainty, Random rnd) { - super(bitLength, certainty, rnd); - } - - @Override - public String toString() { - if (cachedString == null) { - this.cachedString = super.toString(); - } - return cachedString; - } - - @Override - public boolean equals(Object o) { - return super.equals(o); - } - - @Override - public int hashCode() { - return super.hashCode(); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/AbstractDecorator.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/AbstractDecorator.java deleted file mode 100644 index ec43c3b672..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/AbstractDecorator.java +++ /dev/null @@ -1,69 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.opentracing.DDSpanContext; - -/** - * Span decorators are called when new tags are written and proceed to various remappings and - * enrichments - */ -public abstract class AbstractDecorator { - - private String matchingTag; - - private Object matchingValue; - - private String replacementTag; - - private String replacementValue; - - public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) { - if (this.getMatchingValue() == null || this.getMatchingValue().equals(value)) { - final String targetTag = getReplacementTag() == null ? tag : getReplacementTag(); - final String targetValue = - getReplacementValue() == null ? String.valueOf(value) : getReplacementValue(); - - context.setTag(targetTag, targetValue); - return false; - } else { - return true; - } - } - - public String getMatchingTag() { - return matchingTag; - } - - public void setMatchingTag(final String tag) { - this.matchingTag = tag; - } - - public Object getMatchingValue() { - return matchingValue; - } - - public void setMatchingValue(final Object value) { - this.matchingValue = value; - } - - public String getReplacementTag() { - return replacementTag; - } - - public void setReplacementTag(final String targetTag) { - this.replacementTag = targetTag; - } - - public String getReplacementValue() { - return replacementValue; - } - - public void setReplacementValue(final String targetValue) { - this.replacementValue = targetValue; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/DBTypeDecorator.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/DBTypeDecorator.java deleted file mode 100644 index f9bfef6fda..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/DBTypeDecorator.java +++ /dev/null @@ -1,53 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.DDSpanTypes; -import com.datadog.trace.api.DDTags; -import io.opentracing.tag.Tags; - -/** - * This span decorator leverages DB tags. It allows the dev to define a custom service name and - * retrieves some DB meta such as the statement - */ -@Deprecated // This should be covered by instrumentation decorators now. -public class DBTypeDecorator extends AbstractDecorator { - - public DBTypeDecorator() { - super(); - setMatchingTag(Tags.DB_TYPE.getKey()); - setReplacementTag(DDTags.SERVICE_NAME); - } - - @Override - public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) { - - // Assign service name - if (!super.shouldSetTag(context, tag, value)) { - if ("couchbase".equals(value) || "elasticsearch".equals(value)) { - // these instrumentation have different behavior. - return true; - } - // Assign span type to DB - // Special case: Mongo, set to mongodb - if ("mongo".equals(value)) { - // Todo: not sure it's used cos already in the agent mongo helper - context.setSpanType(DDSpanTypes.MONGO); - } else if ("cassandra".equals(value)) { - context.setSpanType(DDSpanTypes.CASSANDRA); - } else if ("memcached".equals(value)) { - context.setSpanType(DDSpanTypes.MEMCACHED); - } else { - context.setSpanType(DDSpanTypes.SQL); - } - // Works for: mongo, cassandra, jdbc - context.setOperationName(String.valueOf(value) + ".query"); - } - return true; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/DDDecoratorsFactory.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/DDDecoratorsFactory.java deleted file mode 100644 index ffe7507436..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/DDDecoratorsFactory.java +++ /dev/null @@ -1,44 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.trace.api.Config; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** Create DDSpanDecorators */ -public class DDDecoratorsFactory { - public static List createBuiltinDecorators() { - - final List decorators = new ArrayList<>(); - - for (final AbstractDecorator decorator : - Arrays.asList( - new DBTypeDecorator(), - new ForceManualDropDecorator(), - new ForceManualKeepDecorator(), - new PeerServiceDecorator(), - new ServiceNameDecorator(), - new ServiceNameDecorator("service", false), - new ServletContextDecorator())) { - - if (Config.get().isRuleEnabled(decorator.getClass().getSimpleName())) { - decorators.add(decorator); - } - } - - // SplitByTags purposely does not check for ServiceNameDecorator being enabled - // This allows for ServiceNameDecorator to be disabled above while keeping SplitByTags - // SplitByTags can be disable by removing SplitByTags config - for (final String splitByTag : Config.get().getSplitByTags()) { - decorators.add(new ServiceNameDecorator(splitByTag, true)); - } - - return decorators; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ForceManualDropDecorator.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ForceManualDropDecorator.java deleted file mode 100644 index 21fa2ab6a4..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ForceManualDropDecorator.java +++ /dev/null @@ -1,32 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.DDTags; -import com.datadog.trace.api.sampling.PrioritySampling; - -/** - * Tag decorator to replace tag 'manual.drop: true' with the appropriate priority sampling value. - */ -public class ForceManualDropDecorator extends AbstractDecorator { - - public ForceManualDropDecorator() { - super(); - setMatchingTag(DDTags.MANUAL_DROP); - } - - @Override - public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) { - if (value instanceof Boolean && (boolean) value) { - context.setSamplingPriority(PrioritySampling.USER_DROP); - } else if (value instanceof String && Boolean.parseBoolean((String) value)) { - context.setSamplingPriority(PrioritySampling.USER_DROP); - } - return false; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ForceManualKeepDecorator.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ForceManualKeepDecorator.java deleted file mode 100644 index 150d1c3967..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ForceManualKeepDecorator.java +++ /dev/null @@ -1,32 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.DDTags; -import com.datadog.trace.api.sampling.PrioritySampling; - -/** - * Tag decorator to replace tag 'manual.keep: true' with the appropriate priority sampling value. - */ -public class ForceManualKeepDecorator extends AbstractDecorator { - - public ForceManualKeepDecorator() { - super(); - setMatchingTag(DDTags.MANUAL_KEEP); - } - - @Override - public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) { - if (value instanceof Boolean && (boolean) value) { - context.setSamplingPriority(PrioritySampling.USER_KEEP); - } else if (value instanceof String && Boolean.parseBoolean((String) value)) { - context.setSamplingPriority(PrioritySampling.USER_KEEP); - } - return false; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/PeerServiceDecorator.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/PeerServiceDecorator.java deleted file mode 100644 index 1e7adbd862..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/PeerServiceDecorator.java +++ /dev/null @@ -1,23 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.opentracing.DDSpanContext; -import io.opentracing.tag.Tags; - -public class PeerServiceDecorator extends AbstractDecorator { - public PeerServiceDecorator() { - super(); - this.setMatchingTag(Tags.PEER_SERVICE.getKey()); - } - - @Override - public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) { - context.setServiceName(String.valueOf(value)); - return false; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ServiceNameDecorator.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ServiceNameDecorator.java deleted file mode 100644 index faad513899..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ServiceNameDecorator.java +++ /dev/null @@ -1,31 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.DDTags; - -public class ServiceNameDecorator extends AbstractDecorator { - - private final boolean setTag; - - public ServiceNameDecorator() { - this(DDTags.SERVICE_NAME, false); - } - - public ServiceNameDecorator(final String splitByTag, final boolean setTag) { - super(); - this.setTag = setTag; - setMatchingTag(splitByTag); - } - - @Override - public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) { - context.setServiceName(String.valueOf(value)); - return setTag; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ServletContextDecorator.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ServletContextDecorator.java deleted file mode 100644 index ec6b3ebb61..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/decorators/ServletContextDecorator.java +++ /dev/null @@ -1,37 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.decorators; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.Config; - -public class ServletContextDecorator extends AbstractDecorator { - - public ServletContextDecorator() { - super(); - setMatchingTag("servlet.context"); - } - - @Override - public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) { - String contextName = String.valueOf(value).trim(); - if (contextName.equals("/") - || (!context.getServiceName().equals(Config.DEFAULT_SERVICE_NAME) - && !context.getServiceName().isEmpty())) { - return true; - } - if (contextName.startsWith("/")) { - if (contextName.length() > 1) { - contextName = contextName.substring(1); - } - } - if (!contextName.isEmpty()) { - context.setServiceName(contextName); - } - return true; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDNoopScopeEvent.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDNoopScopeEvent.java deleted file mode 100644 index 1f82178884..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDNoopScopeEvent.java +++ /dev/null @@ -1,23 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.jfr; - -/** Scope event implementation that does no reporting */ -public final class DDNoopScopeEvent implements DDScopeEvent { - - public static final DDNoopScopeEvent INSTANCE = new DDNoopScopeEvent(); - - @Override - public void start() { - // Noop - } - - @Override - public void finish() { - // Noop - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDNoopScopeEventFactory.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDNoopScopeEventFactory.java deleted file mode 100644 index e8862613d5..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDNoopScopeEventFactory.java +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.jfr; - -import com.datadog.opentracing.DDSpanContext; - -/** Event factory that returns {@link DDNoopScopeEvent} */ -public final class DDNoopScopeEventFactory implements DDScopeEventFactory { - @Override - public DDScopeEvent create(final DDSpanContext context) { - return DDNoopScopeEvent.INSTANCE; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDScopeEvent.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDScopeEvent.java deleted file mode 100644 index 202330b4e0..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDScopeEvent.java +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.jfr; - -/** Scope event */ -public interface DDScopeEvent { - - void start(); - - void finish(); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDScopeEventFactory.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDScopeEventFactory.java deleted file mode 100644 index e481a6ceb7..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/jfr/DDScopeEventFactory.java +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.jfr; - -import com.datadog.opentracing.DDSpanContext; - -/** Factory that produces scope events */ -public interface DDScopeEventFactory { - - /** - * Create new scope event for given context. - * - * @param context span context. - * @return scope event instance - */ - DDScopeEvent create(final DDSpanContext context); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/B3HttpCodec.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/B3HttpCodec.java deleted file mode 100644 index 0de3a9a74b..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/B3HttpCodec.java +++ /dev/null @@ -1,143 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.propagation; - -import static com.datadog.opentracing.propagation.HttpCodec.validateUInt64BitsID; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.sampling.PrioritySampling; -import io.opentracing.SpanContext; -import io.opentracing.propagation.TextMapExtract; -import io.opentracing.propagation.TextMapInject; -import java.math.BigInteger; -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -/** - * A codec designed for HTTP transport via headers using B3 headers - * - *

TODO: there is fair amount of code duplication between DatadogHttpCodec and this class, - * especially in part where TagContext is handled. We may want to refactor that and avoid special - * handling of TagContext in other places (i.e. CompoundExtractor). - */ -class B3HttpCodec { - - private static final String TRACE_ID_KEY = "X-B3-TraceId"; - private static final String SPAN_ID_KEY = "X-B3-SpanId"; - private static final String SAMPLING_PRIORITY_KEY = "X-B3-Sampled"; - private static final String SAMPLING_PRIORITY_ACCEPT = String.valueOf(1); - private static final String SAMPLING_PRIORITY_DROP = String.valueOf(0); - private static final int HEX_RADIX = 16; - - private B3HttpCodec() { - // This class should not be created. This also makes code coverage checks happy. - } - - public static class Injector implements HttpCodec.Injector { - - @Override - public void inject(final DDSpanContext context, final TextMapInject carrier) { - try { - carrier.put(TRACE_ID_KEY, context.getTraceId().toString(HEX_RADIX).toLowerCase(Locale.US)); - carrier.put(SPAN_ID_KEY, context.getSpanId().toString(HEX_RADIX).toLowerCase(Locale.US)); - - if (context.lockSamplingPriority()) { - carrier.put( - SAMPLING_PRIORITY_KEY, convertSamplingPriority(context.getSamplingPriority())); - } - } catch (final NumberFormatException e) { - } - } - - private String convertSamplingPriority(final int samplingPriority) { - return samplingPriority > 0 ? SAMPLING_PRIORITY_ACCEPT : SAMPLING_PRIORITY_DROP; - } - } - - public static class Extractor implements HttpCodec.Extractor { - - private final Map taggedHeaders; - - public Extractor(final Map taggedHeaders) { - this.taggedHeaders = new HashMap<>(); - for (final Map.Entry mapping : taggedHeaders.entrySet()) { - this.taggedHeaders.put(mapping.getKey().trim().toLowerCase(Locale.US), mapping.getValue()); - } - } - - @Override - public SpanContext extract(final TextMapExtract carrier) { - try { - Map tags = Collections.emptyMap(); - BigInteger traceId = BigInteger.ZERO; - BigInteger spanId = BigInteger.ZERO; - int samplingPriority = PrioritySampling.UNSET; - - for (final Map.Entry entry : carrier) { - final String key = entry.getKey().toLowerCase(Locale.US); - final String value = entry.getValue(); - - if (value == null) { - continue; - } - - if (TRACE_ID_KEY.equalsIgnoreCase(key)) { - final String trimmedValue; - final int length = value.length(); - if (length > 32) { - traceId = BigInteger.ZERO; - continue; - } else if (length > 16) { - trimmedValue = value.substring(length - 16); - } else { - trimmedValue = value; - } - traceId = validateUInt64BitsID(trimmedValue, HEX_RADIX); - } else if (SPAN_ID_KEY.equalsIgnoreCase(key)) { - spanId = validateUInt64BitsID(value, HEX_RADIX); - } else if (SAMPLING_PRIORITY_KEY.equalsIgnoreCase(key)) { - samplingPriority = convertSamplingPriority(value); - } - - if (taggedHeaders.containsKey(key)) { - if (tags.isEmpty()) { - tags = new HashMap<>(); - } - tags.put(taggedHeaders.get(key), HttpCodec.decode(value)); - } - } - - if (!BigInteger.ZERO.equals(traceId)) { - final ExtractedContext context = - new ExtractedContext( - traceId, - spanId, - samplingPriority, - null, - Collections.emptyMap(), - tags); - context.lockSamplingPriority(); - - return context; - } else if (!tags.isEmpty()) { - return new TagContext(null, tags); - } - } catch (final RuntimeException e) { - } - - return null; - } - - private int convertSamplingPriority(final String samplingPriority) { - return Integer.parseInt(samplingPriority) == 1 - ? PrioritySampling.SAMPLER_KEEP - : PrioritySampling.SAMPLER_DROP; - } - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/DatadogHttpCodec.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/DatadogHttpCodec.java deleted file mode 100644 index c1c87aaf2b..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/DatadogHttpCodec.java +++ /dev/null @@ -1,121 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.propagation; - -import static com.datadog.opentracing.propagation.HttpCodec.validateUInt64BitsID; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.sampling.PrioritySampling; -import io.opentracing.SpanContext; -import io.opentracing.propagation.TextMapExtract; -import io.opentracing.propagation.TextMapInject; -import java.math.BigInteger; -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -/** A codec designed for HTTP transport via headers using Datadog headers */ -class DatadogHttpCodec { - - private static final String OT_BAGGAGE_PREFIX = "ot-baggage-"; - private static final String TRACE_ID_KEY = "x-datadog-trace-id"; - private static final String SPAN_ID_KEY = "x-datadog-parent-id"; - private static final String SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"; - private static final String ORIGIN_KEY = "x-datadog-origin"; - - private DatadogHttpCodec() { - // This class should not be created. This also makes code coverage checks happy. - } - - public static class Injector implements HttpCodec.Injector { - - @Override - public void inject(final DDSpanContext context, final TextMapInject carrier) { - carrier.put(TRACE_ID_KEY, context.getTraceId().toString()); - carrier.put(SPAN_ID_KEY, context.getSpanId().toString()); - if (context.lockSamplingPriority()) { - carrier.put(SAMPLING_PRIORITY_KEY, String.valueOf(context.getSamplingPriority())); - } - final String origin = context.getOrigin(); - if (origin != null) { - carrier.put(ORIGIN_KEY, origin); - } - - for (final Map.Entry entry : context.baggageItems()) { - carrier.put(OT_BAGGAGE_PREFIX + entry.getKey(), HttpCodec.encode(entry.getValue())); - } - } - } - - public static class Extractor implements HttpCodec.Extractor { - private final Map taggedHeaders; - - public Extractor(final Map taggedHeaders) { - this.taggedHeaders = new HashMap<>(); - for (final Map.Entry mapping : taggedHeaders.entrySet()) { - this.taggedHeaders.put(mapping.getKey().trim().toLowerCase(Locale.US), mapping.getValue()); - } - } - - @Override - public SpanContext extract(final TextMapExtract carrier) { - try { - Map baggage = Collections.emptyMap(); - Map tags = Collections.emptyMap(); - BigInteger traceId = BigInteger.ZERO; - BigInteger spanId = BigInteger.ZERO; - int samplingPriority = PrioritySampling.UNSET; - String origin = null; - - for (final Map.Entry entry : carrier) { - final String key = entry.getKey().toLowerCase(Locale.US); - final String value = entry.getValue(); - - if (value == null) { - continue; - } - - if (TRACE_ID_KEY.equalsIgnoreCase(key)) { - traceId = validateUInt64BitsID(value, 10); - } else if (SPAN_ID_KEY.equalsIgnoreCase(key)) { - spanId = validateUInt64BitsID(value, 10); - } else if (SAMPLING_PRIORITY_KEY.equalsIgnoreCase(key)) { - samplingPriority = Integer.parseInt(value); - } else if (ORIGIN_KEY.equalsIgnoreCase(key)) { - origin = value; - } else if (key.startsWith(OT_BAGGAGE_PREFIX)) { - if (baggage.isEmpty()) { - baggage = new HashMap<>(); - } - baggage.put(key.replace(OT_BAGGAGE_PREFIX, ""), HttpCodec.decode(value)); - } - - if (taggedHeaders.containsKey(key)) { - if (tags.isEmpty()) { - tags = new HashMap<>(); - } - tags.put(taggedHeaders.get(key), HttpCodec.decode(value)); - } - } - - if (!BigInteger.ZERO.equals(traceId)) { - final ExtractedContext context = - new ExtractedContext(traceId, spanId, samplingPriority, origin, baggage, tags); - context.lockSamplingPriority(); - - return context; - } else if (origin != null || !tags.isEmpty()) { - return new TagContext(origin, tags); - } - } catch (final RuntimeException e) { - } - - return null; - } - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/ExtractedContext.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/ExtractedContext.java deleted file mode 100644 index d9ce18ea68..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/ExtractedContext.java +++ /dev/null @@ -1,65 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.propagation; - -import java.math.BigInteger; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Propagated data resulting from calling tracer.extract with header data from an incoming request. - */ -public class ExtractedContext extends TagContext { - private final BigInteger traceId; - private final BigInteger spanId; - private final int samplingPriority; - private final Map baggage; - private final AtomicBoolean samplingPriorityLocked = new AtomicBoolean(false); - - public ExtractedContext( - final BigInteger traceId, - final BigInteger spanId, - final int samplingPriority, - final String origin, - final Map baggage, - final Map tags) { - super(origin, tags); - this.traceId = traceId; - this.spanId = spanId; - this.samplingPriority = samplingPriority; - this.baggage = baggage; - } - - @Override - public Iterable> baggageItems() { - return baggage.entrySet(); - } - - public void lockSamplingPriority() { - samplingPriorityLocked.set(true); - } - - public BigInteger getTraceId() { - return traceId; - } - - public BigInteger getSpanId() { - return spanId; - } - - public int getSamplingPriority() { - return samplingPriority; - } - - public Map getBaggage() { - return baggage; - } - - public boolean getSamplingPriorityLocked() { - return samplingPriorityLocked.get(); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/HaystackHttpCodec.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/HaystackHttpCodec.java deleted file mode 100644 index 709eafa17b..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/HaystackHttpCodec.java +++ /dev/null @@ -1,115 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.propagation; - -import static com.datadog.opentracing.propagation.HttpCodec.validateUInt64BitsID; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.trace.api.sampling.PrioritySampling; -import io.opentracing.SpanContext; -import io.opentracing.propagation.TextMapExtract; -import io.opentracing.propagation.TextMapInject; -import java.math.BigInteger; -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -/** - * A codec designed for HTTP transport via headers using Haystack headers. - * - * @author Alex Antonov - */ -public class HaystackHttpCodec { - - private static final String OT_BAGGAGE_PREFIX = "Baggage-"; - private static final String TRACE_ID_KEY = "Trace-ID"; - private static final String SPAN_ID_KEY = "Span-ID"; - private static final String PARENT_ID_KEY = "Parent_ID"; - - private HaystackHttpCodec() { - // This class should not be created. This also makes code coverage checks happy. - } - - public static class Injector implements HttpCodec.Injector { - - @Override - public void inject(final DDSpanContext context, final TextMapInject carrier) { - carrier.put(TRACE_ID_KEY, context.getTraceId().toString()); - carrier.put(SPAN_ID_KEY, context.getSpanId().toString()); - carrier.put(PARENT_ID_KEY, context.getParentId().toString()); - - for (final Map.Entry entry : context.baggageItems()) { - carrier.put(OT_BAGGAGE_PREFIX + entry.getKey(), HttpCodec.encode(entry.getValue())); - } - } - } - - public static class Extractor implements HttpCodec.Extractor { - private final Map taggedHeaders; - - /** Creates Header Extractor using Haystack propagation. */ - public Extractor(final Map taggedHeaders) { - this.taggedHeaders = new HashMap<>(); - for (final Map.Entry mapping : taggedHeaders.entrySet()) { - this.taggedHeaders.put(mapping.getKey().trim().toLowerCase(Locale.US), mapping.getValue()); - } - } - - @Override - public SpanContext extract(final TextMapExtract carrier) { - try { - Map baggage = Collections.emptyMap(); - Map tags = Collections.emptyMap(); - BigInteger traceId = BigInteger.ZERO; - BigInteger spanId = BigInteger.ZERO; - final int samplingPriority = PrioritySampling.SAMPLER_KEEP; - final String origin = null; // Always null - - for (final Map.Entry entry : carrier) { - final String key = entry.getKey().toLowerCase(Locale.US); - final String value = entry.getValue(); - - if (value == null) { - continue; - } - - if (TRACE_ID_KEY.equalsIgnoreCase(key)) { - traceId = validateUInt64BitsID(value, 10); - } else if (SPAN_ID_KEY.equalsIgnoreCase(key)) { - spanId = validateUInt64BitsID(value, 10); - } else if (key.startsWith(OT_BAGGAGE_PREFIX.toLowerCase(Locale.US))) { - if (baggage.isEmpty()) { - baggage = new HashMap<>(); - } - baggage.put(key.replace(OT_BAGGAGE_PREFIX.toLowerCase(Locale.US), ""), HttpCodec.decode(value)); - } - - if (taggedHeaders.containsKey(key)) { - if (tags.isEmpty()) { - tags = new HashMap<>(); - } - tags.put(taggedHeaders.get(key), HttpCodec.decode(value)); - } - } - - if (!BigInteger.ZERO.equals(traceId)) { - final ExtractedContext context = - new ExtractedContext(traceId, spanId, samplingPriority, origin, baggage, tags); - context.lockSamplingPriority(); - - return context; - } else if (origin != null || !tags.isEmpty()) { - return new TagContext(origin, tags); - } - } catch (final RuntimeException e) { - } - - return null; - } - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/HttpCodec.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/HttpCodec.java deleted file mode 100644 index ec178ab1f4..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/HttpCodec.java +++ /dev/null @@ -1,152 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.propagation; - -import com.datadog.opentracing.DDSpanContext; -import com.datadog.opentracing.DDTracer; -import com.datadog.opentracing.StringCachingBigInteger; -import com.datadog.trace.api.Config; -import io.opentracing.SpanContext; -import io.opentracing.propagation.TextMapExtract; -import io.opentracing.propagation.TextMapInject; -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class HttpCodec { - public interface Injector { - - void inject(final DDSpanContext context, final TextMapInject carrier); - } - - public interface Extractor { - - SpanContext extract(final TextMapExtract carrier); - } - - public static Injector createInjector(final Config config) { - final List injectors = new ArrayList<>(); - for (final Config.PropagationStyle style : config.getPropagationStylesToInject()) { - if (style == Config.PropagationStyle.DATADOG) { - injectors.add(new DatadogHttpCodec.Injector()); - continue; - } - if (style == Config.PropagationStyle.B3) { - injectors.add(new B3HttpCodec.Injector()); - continue; - } - if (style == Config.PropagationStyle.HAYSTACK) { - injectors.add(new HaystackHttpCodec.Injector()); - continue; - } - } - return new CompoundInjector(injectors); - } - - public static Extractor createExtractor( - final Config config, final Map taggedHeaders) { - final List extractors = new ArrayList<>(); - for (final Config.PropagationStyle style : config.getPropagationStylesToExtract()) { - if (style == Config.PropagationStyle.DATADOG) { - extractors.add(new DatadogHttpCodec.Extractor(taggedHeaders)); - continue; - } - if (style == Config.PropagationStyle.B3) { - extractors.add(new B3HttpCodec.Extractor(taggedHeaders)); - continue; - } - if (style == Config.PropagationStyle.HAYSTACK) { - extractors.add(new HaystackHttpCodec.Extractor(taggedHeaders)); - continue; - } - } - return new CompoundExtractor(extractors); - } - - public static class CompoundInjector implements Injector { - - private final List injectors; - - public CompoundInjector(final List injectors) { - this.injectors = injectors; - } - - @Override - public void inject(final DDSpanContext context, final TextMapInject carrier) { - for (final Injector injector : injectors) { - injector.inject(context, carrier); - } - } - } - - public static class CompoundExtractor implements Extractor { - - private final List extractors; - - public CompoundExtractor(final List extractors) { - this.extractors = extractors; - } - - @Override - public SpanContext extract(final TextMapExtract carrier) { - SpanContext context = null; - for (final Extractor extractor : extractors) { - context = extractor.extract(carrier); - // Use incomplete TagContext only as last resort - if (context != null && (context instanceof ExtractedContext)) { - return context; - } - } - return context; - } - } - - /** - * Helper method to validate an ID String to verify within range - * - * @param value the String that contains the ID - * @param radix radix to use to parse the ID - * @return the parsed ID - * @throws IllegalArgumentException if value cannot be converted to integer or doesn't conform to - * required boundaries - */ - static BigInteger validateUInt64BitsID(final String value, final int radix) - throws IllegalArgumentException { - final BigInteger parsedValue = new StringCachingBigInteger(value, radix); - if (parsedValue.compareTo(DDTracer.TRACE_ID_MIN) < 0 - || parsedValue.compareTo(DDTracer.TRACE_ID_MAX) > 0) { - throw new IllegalArgumentException( - "ID out of range, must be between 0 and 2^64-1, got: " + value); - } - - return parsedValue; - } - - /** URL encode value */ - static String encode(final String value) { - String encoded = value; - try { - encoded = URLEncoder.encode(value, "UTF-8"); - } catch (final UnsupportedEncodingException e) { - } - return encoded; - } - - /** URL decode value */ - static String decode(final String value) { - String decoded = value; - try { - decoded = URLDecoder.decode(value, "UTF-8"); - } catch (final UnsupportedEncodingException e) { - } - return decoded; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/TagContext.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/TagContext.java deleted file mode 100644 index dad8c3385a..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/propagation/TagContext.java +++ /dev/null @@ -1,48 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.propagation; - -import io.opentracing.SpanContext; -import java.util.Collections; -import java.util.Map; - -/** - * When calling extract, we allow for grabbing other configured headers as tags. Those tags are - * returned here even if the rest of the request would have returned null. - */ -public class TagContext implements SpanContext { - private final String origin; - private final Map tags; - - public TagContext(final String origin, final Map tags) { - this.origin = origin; - this.tags = tags; - } - - public String getOrigin() { - return origin; - } - - public Map getTags() { - return tags; - } - - @Override - public String toTraceId() { - return ""; - } - - @Override - public String toSpanId() { - return ""; - } - - @Override - public Iterable> baggageItems() { - return Collections.emptyList(); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ContextualScopeManager.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ContextualScopeManager.java deleted file mode 100644 index b4e4da76f1..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ContextualScopeManager.java +++ /dev/null @@ -1,98 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.scopemanager; - -import com.datadog.opentracing.DDSpan; -import com.datadog.opentracing.jfr.DDScopeEventFactory; -import com.datadog.trace.context.ScopeListener; -import io.opentracing.Scope; -import io.opentracing.ScopeManager; -import io.opentracing.Span; -import io.opentracing.noop.NoopScopeManager; -import java.util.Deque; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -public class ContextualScopeManager implements ScopeManager { - static final ThreadLocal tlsScope = new ThreadLocal<>(); - final Deque scopeContexts = new LinkedList<>(); - final List scopeListeners = new CopyOnWriteArrayList<>(); - - private final int depthLimit; - private final DDScopeEventFactory scopeEventFactory; - - public ContextualScopeManager(final int depthLimit, final DDScopeEventFactory scopeEventFactory) { - this.depthLimit = depthLimit; - this.scopeEventFactory = scopeEventFactory; - } - - @Override - public Scope activate(final Span span, final boolean finishOnClose) { - final Scope active = active(); - if (active instanceof DDScope) { - final int currentDepth = ((DDScope) active).depth(); - if (depthLimit <= currentDepth) { - return NoopScopeManager.NoopScope.INSTANCE; - } - } - synchronized (scopeContexts) { - for (final ScopeContext context : scopeContexts) { - if (context.inContext()) { - return context.activate(span, finishOnClose); - } - } - } - if (span instanceof DDSpan) { - return new ContinuableScope(this, (DDSpan) span, finishOnClose, scopeEventFactory); - } else { - return new SimpleScope(this, span, finishOnClose); - } - } - - @Override - public Scope activate(final Span span) { - return activate(span, false); - } - - @Override - public Scope active() { - synchronized (scopeContexts) { - for (final ScopeContext csm : scopeContexts) { - if (csm.inContext()) { - return csm.active(); - } - } - } - return tlsScope.get(); - } - - @Override - public Span activeSpan() { - synchronized (scopeContexts) { - for (final ScopeContext csm : scopeContexts) { - if (csm.inContext()) { - return csm.activeSpan(); - } - } - } - final DDScope active = tlsScope.get(); - return active == null ? null : active.span(); - } - - @Deprecated - public void addScopeContext(final ScopeContext context) { - synchronized (scopeContexts) { - scopeContexts.addFirst(context); - } - } - - /** Attach a listener to scope activation events */ - public void addScopeListener(final ScopeListener listener) { - scopeListeners.add(listener); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ContinuableScope.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ContinuableScope.java deleted file mode 100644 index e95418f8c0..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ContinuableScope.java +++ /dev/null @@ -1,193 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.scopemanager; - -import com.datadog.opentracing.DDSpan; -import com.datadog.opentracing.DDSpanContext; -import com.datadog.opentracing.PendingTrace; -import com.datadog.opentracing.jfr.DDScopeEvent; -import com.datadog.opentracing.jfr.DDScopeEventFactory; -import com.datadog.trace.context.ScopeListener; -import com.datadog.trace.context.TraceScope; -import java.io.Closeable; -import java.lang.ref.WeakReference; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -public class ContinuableScope implements DDScope, TraceScope { - /** ScopeManager holding the thread-local to this scope. */ - private final ContextualScopeManager scopeManager; - /** - * Span contained by this scope. Async scopes will hold a reference to the parent scope's span. - */ - private final DDSpan spanUnderScope; - - private final DDScopeEventFactory eventFactory; - /** Event for this scope */ - private final DDScopeEvent event; - /** If true, finish the span when openCount hits 0. */ - private final boolean finishOnClose; - /** Count of open scope and continuations */ - private final AtomicInteger openCount; - /** Scope to placed in the thread local after close. May be null. */ - private final DDScope toRestore; - /** Continuation that created this scope. May be null. */ - private final Continuation continuation; - /** Flag to propagate this scope across async boundaries. */ - private final AtomicBoolean isAsyncPropagating = new AtomicBoolean(false); - /** depth of scope on thread */ - private final int depth; - - ContinuableScope( - final ContextualScopeManager scopeManager, - final DDSpan spanUnderScope, - final boolean finishOnClose, - final DDScopeEventFactory eventFactory) { - this(scopeManager, new AtomicInteger(1), null, spanUnderScope, finishOnClose, eventFactory); - } - - private ContinuableScope( - final ContextualScopeManager scopeManager, - final AtomicInteger openCount, - final Continuation continuation, - final DDSpan spanUnderScope, - final boolean finishOnClose, - final DDScopeEventFactory eventFactory) { - assert spanUnderScope != null : "span must not be null"; - this.scopeManager = scopeManager; - this.openCount = openCount; - this.continuation = continuation; - this.spanUnderScope = spanUnderScope; - this.finishOnClose = finishOnClose; - this.eventFactory = eventFactory; - event = eventFactory.create(spanUnderScope.context()); - event.start(); - toRestore = scopeManager.tlsScope.get(); - scopeManager.tlsScope.set(this); - depth = toRestore == null ? 0 : toRestore.depth() + 1; - for (final ScopeListener listener : scopeManager.scopeListeners) { - listener.afterScopeActivated(); - } - } - - @Override - public void close() { - // We have to scope finish event before we finish then span (which finishes span event). - // The reason is that we get span on construction and span event starts when span is created. - // This means from JFR perspective scope is included into the span. - event.finish(); - - if (null != continuation) { - spanUnderScope.context().getTrace().cancelContinuation(continuation); - } - - if (openCount.decrementAndGet() == 0 && finishOnClose) { - spanUnderScope.finish(); - } - - for (final ScopeListener listener : scopeManager.scopeListeners) { - listener.afterScopeClosed(); - } - - if (scopeManager.tlsScope.get() == this) { - scopeManager.tlsScope.set(toRestore); - if (toRestore != null) { - for (final ScopeListener listener : scopeManager.scopeListeners) { - listener.afterScopeActivated(); - } - } - } else { - } - } - - @Override - public DDSpan span() { - return spanUnderScope; - } - - @Override - public int depth() { - return depth; - } - - @Override - public boolean isAsyncPropagating() { - return isAsyncPropagating.get(); - } - - @Override - public void setAsyncPropagation(final boolean value) { - isAsyncPropagating.set(value); - } - - /** - * The continuation returned must be closed or activated or the trace will not finish. - * - * @return The new continuation, or null if this scope is not async propagating. - */ - @Override - public Continuation capture() { - if (isAsyncPropagating()) { - return new Continuation(); - } else { - return null; - } - } - - @Override - public String toString() { - return super.toString() + "->" + spanUnderScope; - } - - public class Continuation implements Closeable, TraceScope.Continuation { - public WeakReference ref; - - private final AtomicBoolean used = new AtomicBoolean(false); - private final PendingTrace trace; - - private Continuation() { - openCount.incrementAndGet(); - final DDSpanContext context = spanUnderScope.context(); - trace = context.getTrace(); - trace.registerContinuation(this); - } - - @Override - public ContinuableScope activate() { - if (used.compareAndSet(false, true)) { - final ContinuableScope scope = - new ContinuableScope( - scopeManager, openCount, this, spanUnderScope, finishOnClose, eventFactory); - return scope; - } else { - return new ContinuableScope( - scopeManager, new AtomicInteger(1), null, spanUnderScope, finishOnClose, eventFactory); - } - } - - @Override - public void close() { - close(true); - } - - @Override - public void close(final boolean closeContinuationScope) { - if (used.compareAndSet(false, true)) { - trace.cancelContinuation(this); - if (closeContinuationScope) { - ContinuableScope.this.close(); - } else { - // Same as in 'close()' above. - if (openCount.decrementAndGet() == 0 && finishOnClose) { - spanUnderScope.finish(); - } - } - } else { - } - } - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/DDScope.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/DDScope.java deleted file mode 100644 index b59aafbe3e..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/DDScope.java +++ /dev/null @@ -1,18 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.scopemanager; - -import io.opentracing.Scope; -import io.opentracing.Span; - -// Intentionally package private. -interface DDScope extends Scope { - @Override - Span span(); - - int depth(); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ScopeContext.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ScopeContext.java deleted file mode 100644 index 49146eb41b..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/ScopeContext.java +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.scopemanager; - -import io.opentracing.ScopeManager; - -/** Represents a ScopeManager that is only valid in certain cases such as on a specific thread. */ -@Deprecated -public interface ScopeContext extends ScopeManager { - - /** - * When multiple ScopeContexts are active, the first one to respond true will have control. - * - * @return true if this ScopeContext should be active - */ - boolean inContext(); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/SimpleScope.java b/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/SimpleScope.java deleted file mode 100644 index 66043749a6..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/opentracing/scopemanager/SimpleScope.java +++ /dev/null @@ -1,64 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.opentracing.scopemanager; - -import com.datadog.trace.context.ScopeListener; -import io.opentracing.Span; - -/** Simple scope implementation which does not propagate across threads. */ -public class SimpleScope implements DDScope { - private final ContextualScopeManager scopeManager; - private final Span spanUnderScope; - private final boolean finishOnClose; - private final DDScope toRestore; - private final int depth; - - public SimpleScope( - final ContextualScopeManager scopeManager, - final Span spanUnderScope, - final boolean finishOnClose) { - assert spanUnderScope != null : "span must not be null"; - this.scopeManager = scopeManager; - this.spanUnderScope = spanUnderScope; - this.finishOnClose = finishOnClose; - toRestore = scopeManager.tlsScope.get(); - scopeManager.tlsScope.set(this); - depth = toRestore == null ? 0 : toRestore.depth() + 1; - for (final ScopeListener listener : scopeManager.scopeListeners) { - listener.afterScopeActivated(); - } - } - - @Override - public void close() { - if (finishOnClose) { - spanUnderScope.finish(); - } - for (final ScopeListener listener : scopeManager.scopeListeners) { - listener.afterScopeClosed(); - } - - if (scopeManager.tlsScope.get() == this) { - scopeManager.tlsScope.set(toRestore); - if (toRestore != null) { - for (final ScopeListener listener : scopeManager.scopeListeners) { - listener.afterScopeActivated(); - } - } - } - } - - @Override - public Span span() { - return spanUnderScope; - } - - @Override - public int depth() { - return depth; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/Config.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/Config.java deleted file mode 100644 index 700e44704b..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/Config.java +++ /dev/null @@ -1,1652 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.reflect.Method; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.*; -import java.util.regex.Pattern; - -/** - * Config reads values with the following priority: 1) system properties, 2) environment variables, - * 3) optional configuration file. It also includes default values to ensure a valid config. - * - *

- * - *

System properties are {@link Config#PREFIX}'ed. Environment variables are the same as the - * system property, but uppercased with '.' -> '_'. - */ -public class Config { - /** - * Config keys below - */ - private static final String PREFIX = "dd."; - - public static final String PROFILING_URL_TEMPLATE = "https://intake.profile.%s/v1/input"; - - private static final Pattern ENV_REPLACEMENT = Pattern.compile("[^a-zA-Z0-9_]"); - - public static final String CONFIGURATION_FILE = "trace.config"; - public static final String API_KEY = "api-key"; - public static final String API_KEY_FILE = "api-key-file"; - public static final String SITE = "site"; - public static final String SERVICE_NAME = "service.name"; - public static final String TRACE_ENABLED = "trace.enabled"; - public static final String INTEGRATIONS_ENABLED = "integrations.enabled"; - public static final String WRITER_TYPE = "writer.type"; - public static final String AGENT_HOST = "agent.host"; - public static final String TRACE_AGENT_PORT = "trace.agent.port"; - public static final String AGENT_PORT_LEGACY = "agent.port"; - public static final String AGENT_UNIX_DOMAIN_SOCKET = "trace.agent.unix.domain.socket"; - public static final String PRIORITY_SAMPLING = "priority.sampling"; - public static final String TRACE_RESOLVER_ENABLED = "trace.resolver.enabled"; - public static final String SERVICE_MAPPING = "service.mapping"; - - private static final String ENV = "env"; - private static final String VERSION = "version"; - public static final String TAGS = "tags"; - @Deprecated // Use dd.tags instead - public static final String GLOBAL_TAGS = "trace.global.tags"; - public static final String SPAN_TAGS = "trace.span.tags"; - public static final String JMX_TAGS = "trace.jmx.tags"; - public static final String TRACE_ANALYTICS_ENABLED = "trace.analytics.enabled"; - public static final String TRACE_ANNOTATIONS = "trace.annotations"; - public static final String TRACE_EXECUTORS_ALL = "trace.executors.all"; - public static final String TRACE_EXECUTORS = "trace.executors"; - public static final String TRACE_METHODS = "trace.methods"; - public static final String TRACE_CLASSES_EXCLUDE = "trace.classes.exclude"; - public static final String TRACE_SAMPLING_SERVICE_RULES = "trace.sampling.service.rules"; - public static final String TRACE_SAMPLING_OPERATION_RULES = "trace.sampling.operation.rules"; - public static final String TRACE_SAMPLE_RATE = "trace.sample.rate"; - public static final String TRACE_RATE_LIMIT = "trace.rate.limit"; - public static final String TRACE_REPORT_HOSTNAME = "trace.report-hostname"; - public static final String HEADER_TAGS = "trace.header.tags"; - public static final String HTTP_SERVER_ERROR_STATUSES = "http.server.error.statuses"; - public static final String HTTP_CLIENT_ERROR_STATUSES = "http.client.error.statuses"; - public static final String HTTP_SERVER_TAG_QUERY_STRING = "http.server.tag.query-string"; - public static final String HTTP_CLIENT_TAG_QUERY_STRING = "http.client.tag.query-string"; - public static final String HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN = "trace.http.client.split-by-domain"; - public static final String DB_CLIENT_HOST_SPLIT_BY_INSTANCE = "trace.db.client.split-by-instance"; - public static final String SPLIT_BY_TAGS = "trace.split-by-tags"; - public static final String SCOPE_DEPTH_LIMIT = "trace.scope.depth.limit"; - public static final String PARTIAL_FLUSH_MIN_SPANS = "trace.partial.flush.min.spans"; - public static final String RUNTIME_CONTEXT_FIELD_INJECTION = - "trace.runtime.context.field.injection"; - public static final String PROPAGATION_STYLE_EXTRACT = "propagation.style.extract"; - public static final String PROPAGATION_STYLE_INJECT = "propagation.style.inject"; - - public static final String JMX_FETCH_ENABLED = "jmxfetch.enabled"; - public static final String JMX_FETCH_CONFIG_DIR = "jmxfetch.config.dir"; - public static final String JMX_FETCH_CONFIG = "jmxfetch.config"; - @Deprecated - public static final String JMX_FETCH_METRICS_CONFIGS = "jmxfetch.metrics-configs"; - public static final String JMX_FETCH_CHECK_PERIOD = "jmxfetch.check-period"; - public static final String JMX_FETCH_REFRESH_BEANS_PERIOD = "jmxfetch.refresh-beans-period"; - public static final String JMX_FETCH_STATSD_HOST = "jmxfetch.statsd.host"; - public static final String JMX_FETCH_STATSD_PORT = "jmxfetch.statsd.port"; - - public static final String HEALTH_METRICS_ENABLED = "trace.health.metrics.enabled"; - public static final String HEALTH_METRICS_STATSD_HOST = "trace.health.metrics.statsd.host"; - public static final String HEALTH_METRICS_STATSD_PORT = "trace.health.metrics.statsd.port"; - - public static final String LOGS_INJECTION_ENABLED = "logs.injection"; - - public static final String PROFILING_ENABLED = "profiling.enabled"; - @Deprecated // Use dd.site instead - public static final String PROFILING_URL = "profiling.url"; - @Deprecated // Use dd.api-key instead - public static final String PROFILING_API_KEY_OLD = "profiling.api-key"; - @Deprecated // Use dd.api-key-file instead - public static final String PROFILING_API_KEY_FILE_OLD = "profiling.api-key-file"; - @Deprecated // Use dd.api-key instead - public static final String PROFILING_API_KEY_VERY_OLD = "profiling.apikey"; - @Deprecated // Use dd.api-key-file instead - public static final String PROFILING_API_KEY_FILE_VERY_OLD = "profiling.apikey.file"; - public static final String PROFILING_TAGS = "profiling.tags"; - public static final String PROFILING_START_DELAY = "profiling.start-delay"; - // DANGEROUS! May lead on sigsegv on JVMs before 14 - // Not intended for production use - public static final String PROFILING_START_FORCE_FIRST = - "profiling.experimental.start-force-first"; - public static final String PROFILING_UPLOAD_PERIOD = "profiling.upload.period"; - public static final String PROFILING_TEMPLATE_OVERRIDE_FILE = - "profiling.jfr-template-override-file"; - public static final String PROFILING_UPLOAD_TIMEOUT = "profiling.upload.timeout"; - public static final String PROFILING_UPLOAD_COMPRESSION = "profiling.upload.compression"; - public static final String PROFILING_PROXY_HOST = "profiling.proxy.host"; - public static final String PROFILING_PROXY_PORT = "profiling.proxy.port"; - public static final String PROFILING_PROXY_USERNAME = "profiling.proxy.username"; - public static final String PROFILING_PROXY_PASSWORD = "profiling.proxy.password"; - public static final String PROFILING_EXCEPTION_SAMPLE_LIMIT = "profiling.exception.sample.limit"; - public static final String PROFILING_EXCEPTION_HISTOGRAM_TOP_ITEMS = - "profiling.exception.histogram.top-items"; - public static final String PROFILING_EXCEPTION_HISTOGRAM_MAX_COLLECTION_SIZE = - "profiling.exception.histogram.max-collection-size"; - - public static final String RUNTIME_ID_TAG = "runtime-id"; - public static final String SERVICE = "service"; - public static final String SERVICE_TAG = SERVICE; - public static final String HOST_TAG = "host"; - public static final String LANGUAGE_TAG_KEY = "language"; - public static final String LANGUAGE_TAG_VALUE = "jvm"; - - public static final String DEFAULT_SITE = "datadoghq.com"; - public static final String DEFAULT_SERVICE_NAME = "unnamed-java-app"; - - private static final boolean DEFAULT_TRACE_ENABLED = true; - public static final boolean DEFAULT_INTEGRATIONS_ENABLED = true; - public static final String DD_AGENT_WRITER_TYPE = "DDAgentWriter"; - public static final String LOGGING_WRITER_TYPE = "LoggingWriter"; - private static final String DEFAULT_AGENT_WRITER_TYPE = DD_AGENT_WRITER_TYPE; - - public static final String DEFAULT_AGENT_HOST = "localhost"; - public static final int DEFAULT_TRACE_AGENT_PORT = 8126; - public static final String DEFAULT_AGENT_UNIX_DOMAIN_SOCKET = null; - - private static final boolean DEFAULT_RUNTIME_CONTEXT_FIELD_INJECTION = true; - - private static final boolean DEFAULT_PRIORITY_SAMPLING_ENABLED = true; - private static final boolean DEFAULT_TRACE_RESOLVER_ENABLED = true; - private static final Set DEFAULT_HTTP_SERVER_ERROR_STATUSES = - parseIntegerRangeSet("500-599", "default"); - private static final Set DEFAULT_HTTP_CLIENT_ERROR_STATUSES = - parseIntegerRangeSet("400-499", "default"); - private static final boolean DEFAULT_HTTP_SERVER_TAG_QUERY_STRING = false; - private static final boolean DEFAULT_HTTP_CLIENT_TAG_QUERY_STRING = false; - private static final boolean DEFAULT_HTTP_CLIENT_SPLIT_BY_DOMAIN = false; - private static final boolean DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE = false; - private static final String DEFAULT_SPLIT_BY_TAGS = ""; - private static final int DEFAULT_SCOPE_DEPTH_LIMIT = 100; - private static final int DEFAULT_PARTIAL_FLUSH_MIN_SPANS = 1000; - private static final String DEFAULT_PROPAGATION_STYLE_EXTRACT = PropagationStyle.DATADOG.name(); - private static final String DEFAULT_PROPAGATION_STYLE_INJECT = PropagationStyle.DATADOG.name(); - private static final boolean DEFAULT_JMX_FETCH_ENABLED = true; - - public static final int DEFAULT_JMX_FETCH_STATSD_PORT = 8125; - - public static final boolean DEFAULT_METRICS_ENABLED = false; - // No default constants for metrics statsd support -- falls back to jmxfetch values - - public static final boolean DEFAULT_LOGS_INJECTION_ENABLED = false; - - public static final boolean DEFAULT_PROFILING_ENABLED = false; - public static final int DEFAULT_PROFILING_START_DELAY = 10; - public static final boolean DEFAULT_PROFILING_START_FORCE_FIRST = false; - public static final int DEFAULT_PROFILING_UPLOAD_PERIOD = 60; // 1 min - public static final int DEFAULT_PROFILING_UPLOAD_TIMEOUT = 30; // seconds - public static final String DEFAULT_PROFILING_UPLOAD_COMPRESSION = "on"; - public static final int DEFAULT_PROFILING_PROXY_PORT = 8080; - public static final int DEFAULT_PROFILING_EXCEPTION_SAMPLE_LIMIT = 10_000; - public static final int DEFAULT_PROFILING_EXCEPTION_HISTOGRAM_TOP_ITEMS = 50; - public static final int DEFAULT_PROFILING_EXCEPTION_HISTOGRAM_MAX_COLLECTION_SIZE = 10000; - - private static final String SPLIT_BY_SPACE_OR_COMMA_REGEX = "[,\\s]+"; - - private static final boolean DEFAULT_TRACE_REPORT_HOSTNAME = false; - private static final String DEFAULT_TRACE_ANNOTATIONS = null; - private static final boolean DEFAULT_TRACE_EXECUTORS_ALL = false; - private static final String DEFAULT_TRACE_EXECUTORS = ""; - private static final String DEFAULT_TRACE_METHODS = null; - public static final boolean DEFAULT_TRACE_ANALYTICS_ENABLED = false; - public static final float DEFAULT_ANALYTICS_SAMPLE_RATE = 1.0f; - public static final double DEFAULT_TRACE_RATE_LIMIT = 100; - - public enum PropagationStyle { - DATADOG, - B3, - HAYSTACK - } - - /** - * A tag intended for internal use only, hence not added to the public api DDTags class. - */ - private static final String INTERNAL_HOST_NAME = "_dd.hostname"; - - /** - * this is a random UUID that gets generated on JVM start up and is attached to every root span - * and every JMX metric that is sent out. - */ - private final String runtimeId; - - /** - * Note: this has effect only on profiling site. Traces are sent to Datadog agent and are not - * affected by this setting. - */ - private final String site; - - private final String serviceName; - private final boolean traceEnabled; - private final boolean integrationsEnabled; - private final String writerType; - private final String agentHost; - private final int agentPort; - private final String agentUnixDomainSocket; - private final boolean prioritySamplingEnabled; - private final boolean traceResolverEnabled; - private final Map serviceMapping; - private final Map tags; - private final Map spanTags; - private final Map jmxTags; - private final List excludedClasses; - private final Map headerTags; - private final Set httpServerErrorStatuses; - private final Set httpClientErrorStatuses; - private final boolean httpServerTagQueryString; - private final boolean httpClientTagQueryString; - private final boolean httpClientSplitByDomain; - private final boolean dbClientSplitByInstance; - private final Set splitByTags; - private final Integer scopeDepthLimit; - private final Integer partialFlushMinSpans; - private final boolean runtimeContextFieldInjection; - private final Set propagationStylesToExtract; - private final Set propagationStylesToInject; - - private final boolean jmxFetchEnabled; - private final String jmxFetchConfigDir; - private final List jmxFetchConfigs; - @Deprecated - private final List jmxFetchMetricsConfigs; - private final Integer jmxFetchCheckPeriod; - private final Integer jmxFetchRefreshBeansPeriod; - private final String jmxFetchStatsdHost; - private final Integer jmxFetchStatsdPort; - - // These values are default-ed to those of jmx fetch values as needed - private final boolean healthMetricsEnabled; - private final String healthMetricsStatsdHost; - private final Integer healthMetricsStatsdPort; - - private final boolean logsInjectionEnabled; - private final boolean reportHostName; - - private final String traceAnnotations; - - private final String traceMethods; - - private final boolean traceExecutorsAll; - private final List traceExecutors; - - private final boolean traceAnalyticsEnabled; - - private final Map traceSamplingServiceRules; - private final Map traceSamplingOperationRules; - private final Double traceSampleRate; - private final Double traceRateLimit; - - private final boolean profilingEnabled; - @Deprecated - private final String profilingUrl; - private final Map profilingTags; - private final int profilingStartDelay; - private final boolean profilingStartForceFirst; - private final int profilingUploadPeriod; - private final String profilingTemplateOverrideFile; - private final int profilingUploadTimeout; - private final String profilingUploadCompression; - private final String profilingProxyHost; - private final int profilingProxyPort; - private final String profilingProxyUsername; - private final String profilingProxyPassword; - private final int profilingExceptionSampleLimit; - private final int profilingExceptionHistogramTopItems; - private final int profilingExceptionHistogramMaxCollectionSize; - - // Values from an optionally provided properties file - private static Properties propertiesFromConfigFile; - - // Read order: System Properties -> Env Variables, [-> properties file], [-> default value] - // Visible for testing - Config() { - propertiesFromConfigFile = loadConfigurationFile(); - - runtimeId = UUID.randomUUID().toString(); - - site = getSettingFromEnvironment(SITE, DEFAULT_SITE); - serviceName = - getSettingFromEnvironment( - SERVICE, getSettingFromEnvironment(SERVICE_NAME, DEFAULT_SERVICE_NAME)); - - traceEnabled = getBooleanSettingFromEnvironment(TRACE_ENABLED, DEFAULT_TRACE_ENABLED); - integrationsEnabled = - getBooleanSettingFromEnvironment(INTEGRATIONS_ENABLED, DEFAULT_INTEGRATIONS_ENABLED); - writerType = getSettingFromEnvironment(WRITER_TYPE, DEFAULT_AGENT_WRITER_TYPE); - agentHost = getSettingFromEnvironment(AGENT_HOST, DEFAULT_AGENT_HOST); - agentPort = - getIntegerSettingFromEnvironment( - TRACE_AGENT_PORT, - getIntegerSettingFromEnvironment(AGENT_PORT_LEGACY, DEFAULT_TRACE_AGENT_PORT)); - agentUnixDomainSocket = - getSettingFromEnvironment(AGENT_UNIX_DOMAIN_SOCKET, DEFAULT_AGENT_UNIX_DOMAIN_SOCKET); - prioritySamplingEnabled = - getBooleanSettingFromEnvironment(PRIORITY_SAMPLING, DEFAULT_PRIORITY_SAMPLING_ENABLED); - traceResolverEnabled = - getBooleanSettingFromEnvironment(TRACE_RESOLVER_ENABLED, DEFAULT_TRACE_RESOLVER_ENABLED); - serviceMapping = getMapSettingFromEnvironment(SERVICE_MAPPING, null); - - { - final Map tags = - new HashMap<>(getMapSettingFromEnvironment(GLOBAL_TAGS, null)); - tags.putAll(getMapSettingFromEnvironment(TAGS, null)); - this.tags = getMapWithPropertiesDefinedByEnvironment(tags, ENV, VERSION); - } - - spanTags = getMapSettingFromEnvironment(SPAN_TAGS, null); - jmxTags = getMapSettingFromEnvironment(JMX_TAGS, null); - - excludedClasses = getListSettingFromEnvironment(TRACE_CLASSES_EXCLUDE, null); - headerTags = getMapSettingFromEnvironment(HEADER_TAGS, null); - - httpServerErrorStatuses = - getIntegerRangeSettingFromEnvironment( - HTTP_SERVER_ERROR_STATUSES, DEFAULT_HTTP_SERVER_ERROR_STATUSES); - - httpClientErrorStatuses = - getIntegerRangeSettingFromEnvironment( - HTTP_CLIENT_ERROR_STATUSES, DEFAULT_HTTP_CLIENT_ERROR_STATUSES); - - httpServerTagQueryString = - getBooleanSettingFromEnvironment( - HTTP_SERVER_TAG_QUERY_STRING, DEFAULT_HTTP_SERVER_TAG_QUERY_STRING); - - httpClientTagQueryString = - getBooleanSettingFromEnvironment( - HTTP_CLIENT_TAG_QUERY_STRING, DEFAULT_HTTP_CLIENT_TAG_QUERY_STRING); - - httpClientSplitByDomain = - getBooleanSettingFromEnvironment( - HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, DEFAULT_HTTP_CLIENT_SPLIT_BY_DOMAIN); - - dbClientSplitByInstance = - getBooleanSettingFromEnvironment( - DB_CLIENT_HOST_SPLIT_BY_INSTANCE, DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE); - - splitByTags = - Collections.unmodifiableSet( - new LinkedHashSet<>( - getListSettingFromEnvironment(SPLIT_BY_TAGS, DEFAULT_SPLIT_BY_TAGS))); - - scopeDepthLimit = - getIntegerSettingFromEnvironment(SCOPE_DEPTH_LIMIT, DEFAULT_SCOPE_DEPTH_LIMIT); - - partialFlushMinSpans = - getIntegerSettingFromEnvironment(PARTIAL_FLUSH_MIN_SPANS, DEFAULT_PARTIAL_FLUSH_MIN_SPANS); - - runtimeContextFieldInjection = - getBooleanSettingFromEnvironment( - RUNTIME_CONTEXT_FIELD_INJECTION, DEFAULT_RUNTIME_CONTEXT_FIELD_INJECTION); - - propagationStylesToExtract = - getPropagationStyleSetSettingFromEnvironmentOrDefault( - PROPAGATION_STYLE_EXTRACT, DEFAULT_PROPAGATION_STYLE_EXTRACT); - propagationStylesToInject = - getPropagationStyleSetSettingFromEnvironmentOrDefault( - PROPAGATION_STYLE_INJECT, DEFAULT_PROPAGATION_STYLE_INJECT); - - jmxFetchEnabled = - getBooleanSettingFromEnvironment(JMX_FETCH_ENABLED, DEFAULT_JMX_FETCH_ENABLED); - jmxFetchConfigDir = getSettingFromEnvironment(JMX_FETCH_CONFIG_DIR, null); - jmxFetchConfigs = getListSettingFromEnvironment(JMX_FETCH_CONFIG, null); - jmxFetchMetricsConfigs = getListSettingFromEnvironment(JMX_FETCH_METRICS_CONFIGS, null); - jmxFetchCheckPeriod = getIntegerSettingFromEnvironment(JMX_FETCH_CHECK_PERIOD, null); - jmxFetchRefreshBeansPeriod = - getIntegerSettingFromEnvironment(JMX_FETCH_REFRESH_BEANS_PERIOD, null); - jmxFetchStatsdHost = getSettingFromEnvironment(JMX_FETCH_STATSD_HOST, null); - jmxFetchStatsdPort = - getIntegerSettingFromEnvironment(JMX_FETCH_STATSD_PORT, DEFAULT_JMX_FETCH_STATSD_PORT); - - // Writer.Builder createMonitor will use the values of the JMX fetch & agent to fill-in defaults - healthMetricsEnabled = - getBooleanSettingFromEnvironment(HEALTH_METRICS_ENABLED, DEFAULT_METRICS_ENABLED); - healthMetricsStatsdHost = getSettingFromEnvironment(HEALTH_METRICS_STATSD_HOST, null); - healthMetricsStatsdPort = getIntegerSettingFromEnvironment(HEALTH_METRICS_STATSD_PORT, null); - - logsInjectionEnabled = - getBooleanSettingFromEnvironment(LOGS_INJECTION_ENABLED, DEFAULT_LOGS_INJECTION_ENABLED); - reportHostName = - getBooleanSettingFromEnvironment(TRACE_REPORT_HOSTNAME, DEFAULT_TRACE_REPORT_HOSTNAME); - - traceAnnotations = getSettingFromEnvironment(TRACE_ANNOTATIONS, DEFAULT_TRACE_ANNOTATIONS); - - traceMethods = getSettingFromEnvironment(TRACE_METHODS, DEFAULT_TRACE_METHODS); - - traceExecutorsAll = - getBooleanSettingFromEnvironment(TRACE_EXECUTORS_ALL, DEFAULT_TRACE_EXECUTORS_ALL); - - traceExecutors = getListSettingFromEnvironment(TRACE_EXECUTORS, DEFAULT_TRACE_EXECUTORS); - - traceAnalyticsEnabled = - getBooleanSettingFromEnvironment(TRACE_ANALYTICS_ENABLED, DEFAULT_TRACE_ANALYTICS_ENABLED); - - traceSamplingServiceRules = getMapSettingFromEnvironment(TRACE_SAMPLING_SERVICE_RULES, null); - traceSamplingOperationRules = - getMapSettingFromEnvironment(TRACE_SAMPLING_OPERATION_RULES, null); - traceSampleRate = getDoubleSettingFromEnvironment(TRACE_SAMPLE_RATE, null); - traceRateLimit = getDoubleSettingFromEnvironment(TRACE_RATE_LIMIT, DEFAULT_TRACE_RATE_LIMIT); - - profilingEnabled = - getBooleanSettingFromEnvironment(PROFILING_ENABLED, DEFAULT_PROFILING_ENABLED); - profilingUrl = getSettingFromEnvironment(PROFILING_URL, null); - - profilingTags = getMapSettingFromEnvironment(PROFILING_TAGS, null); - profilingStartDelay = - getIntegerSettingFromEnvironment(PROFILING_START_DELAY, DEFAULT_PROFILING_START_DELAY); - profilingStartForceFirst = - getBooleanSettingFromEnvironment( - PROFILING_START_FORCE_FIRST, DEFAULT_PROFILING_START_FORCE_FIRST); - profilingUploadPeriod = - getIntegerSettingFromEnvironment(PROFILING_UPLOAD_PERIOD, DEFAULT_PROFILING_UPLOAD_PERIOD); - profilingTemplateOverrideFile = - getSettingFromEnvironment(PROFILING_TEMPLATE_OVERRIDE_FILE, null); - profilingUploadTimeout = - getIntegerSettingFromEnvironment( - PROFILING_UPLOAD_TIMEOUT, DEFAULT_PROFILING_UPLOAD_TIMEOUT); - profilingUploadCompression = - getSettingFromEnvironment( - PROFILING_UPLOAD_COMPRESSION, DEFAULT_PROFILING_UPLOAD_COMPRESSION); - profilingProxyHost = getSettingFromEnvironment(PROFILING_PROXY_HOST, null); - profilingProxyPort = - getIntegerSettingFromEnvironment(PROFILING_PROXY_PORT, DEFAULT_PROFILING_PROXY_PORT); - profilingProxyUsername = getSettingFromEnvironment(PROFILING_PROXY_USERNAME, null); - profilingProxyPassword = getSettingFromEnvironment(PROFILING_PROXY_PASSWORD, null); - - profilingExceptionSampleLimit = - getIntegerSettingFromEnvironment( - PROFILING_EXCEPTION_SAMPLE_LIMIT, DEFAULT_PROFILING_EXCEPTION_SAMPLE_LIMIT); - profilingExceptionHistogramTopItems = - getIntegerSettingFromEnvironment( - PROFILING_EXCEPTION_HISTOGRAM_TOP_ITEMS, - DEFAULT_PROFILING_EXCEPTION_HISTOGRAM_TOP_ITEMS); - profilingExceptionHistogramMaxCollectionSize = - getIntegerSettingFromEnvironment( - PROFILING_EXCEPTION_HISTOGRAM_MAX_COLLECTION_SIZE, - DEFAULT_PROFILING_EXCEPTION_HISTOGRAM_MAX_COLLECTION_SIZE); - } - - // Read order: Properties -> Parent - private Config(final Properties properties, final Config parent) { - runtimeId = parent.runtimeId; - - site = properties.getProperty(SITE, parent.site); - serviceName = - properties.getProperty(SERVICE, properties.getProperty(SERVICE_NAME, parent.serviceName)); - - traceEnabled = getPropertyBooleanValue(properties, TRACE_ENABLED, parent.traceEnabled); - integrationsEnabled = - getPropertyBooleanValue(properties, INTEGRATIONS_ENABLED, parent.integrationsEnabled); - writerType = properties.getProperty(WRITER_TYPE, parent.writerType); - agentHost = properties.getProperty(AGENT_HOST, parent.agentHost); - agentPort = - getPropertyIntegerValue( - properties, - TRACE_AGENT_PORT, - getPropertyIntegerValue(properties, AGENT_PORT_LEGACY, parent.agentPort)); - agentUnixDomainSocket = - properties.getProperty(AGENT_UNIX_DOMAIN_SOCKET, parent.agentUnixDomainSocket); - prioritySamplingEnabled = - getPropertyBooleanValue(properties, PRIORITY_SAMPLING, parent.prioritySamplingEnabled); - traceResolverEnabled = - getPropertyBooleanValue(properties, TRACE_RESOLVER_ENABLED, parent.traceResolverEnabled); - serviceMapping = getPropertyMapValue(properties, SERVICE_MAPPING, parent.serviceMapping); - - { - final Map preTags = - new HashMap<>( - getPropertyMapValue(properties, GLOBAL_TAGS, Collections.emptyMap())); - preTags.putAll(getPropertyMapValue(properties, TAGS, parent.tags)); - this.tags = overwriteKeysFromProperties(preTags, properties, ENV, VERSION); - } - spanTags = getPropertyMapValue(properties, SPAN_TAGS, parent.spanTags); - jmxTags = getPropertyMapValue(properties, JMX_TAGS, parent.jmxTags); - excludedClasses = - getPropertyListValue(properties, TRACE_CLASSES_EXCLUDE, parent.excludedClasses); - headerTags = getPropertyMapValue(properties, HEADER_TAGS, parent.headerTags); - - httpServerErrorStatuses = - getPropertyIntegerRangeValue( - properties, HTTP_SERVER_ERROR_STATUSES, parent.httpServerErrorStatuses); - - httpClientErrorStatuses = - getPropertyIntegerRangeValue( - properties, HTTP_CLIENT_ERROR_STATUSES, parent.httpClientErrorStatuses); - - httpServerTagQueryString = - getPropertyBooleanValue( - properties, HTTP_SERVER_TAG_QUERY_STRING, parent.httpServerTagQueryString); - - httpClientTagQueryString = - getPropertyBooleanValue( - properties, HTTP_CLIENT_TAG_QUERY_STRING, parent.httpClientTagQueryString); - - httpClientSplitByDomain = - getPropertyBooleanValue( - properties, HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, parent.httpClientSplitByDomain); - - dbClientSplitByInstance = - getPropertyBooleanValue( - properties, DB_CLIENT_HOST_SPLIT_BY_INSTANCE, parent.dbClientSplitByInstance); - - splitByTags = - Collections.unmodifiableSet( - new LinkedHashSet<>( - getPropertyListValue( - properties, SPLIT_BY_TAGS, new ArrayList<>(parent.splitByTags)))); - - scopeDepthLimit = - getPropertyIntegerValue(properties, SCOPE_DEPTH_LIMIT, parent.scopeDepthLimit); - - partialFlushMinSpans = - getPropertyIntegerValue(properties, PARTIAL_FLUSH_MIN_SPANS, parent.partialFlushMinSpans); - - runtimeContextFieldInjection = - getPropertyBooleanValue( - properties, RUNTIME_CONTEXT_FIELD_INJECTION, parent.runtimeContextFieldInjection); - - final Set parsedPropagationStylesToExtract = - getPropagationStyleSetFromPropertyValue(properties, PROPAGATION_STYLE_EXTRACT); - propagationStylesToExtract = - parsedPropagationStylesToExtract == null - ? parent.propagationStylesToExtract - : parsedPropagationStylesToExtract; - final Set parsedPropagationStylesToInject = - getPropagationStyleSetFromPropertyValue(properties, PROPAGATION_STYLE_INJECT); - propagationStylesToInject = - parsedPropagationStylesToInject == null - ? parent.propagationStylesToInject - : parsedPropagationStylesToInject; - - jmxFetchEnabled = - getPropertyBooleanValue(properties, JMX_FETCH_ENABLED, parent.jmxFetchEnabled); - jmxFetchConfigDir = properties.getProperty(JMX_FETCH_CONFIG_DIR, parent.jmxFetchConfigDir); - jmxFetchConfigs = getPropertyListValue(properties, JMX_FETCH_CONFIG, parent.jmxFetchConfigs); - jmxFetchMetricsConfigs = - getPropertyListValue(properties, JMX_FETCH_METRICS_CONFIGS, parent.jmxFetchMetricsConfigs); - jmxFetchCheckPeriod = - getPropertyIntegerValue(properties, JMX_FETCH_CHECK_PERIOD, parent.jmxFetchCheckPeriod); - jmxFetchRefreshBeansPeriod = - getPropertyIntegerValue( - properties, JMX_FETCH_REFRESH_BEANS_PERIOD, parent.jmxFetchRefreshBeansPeriod); - jmxFetchStatsdHost = properties.getProperty(JMX_FETCH_STATSD_HOST, parent.jmxFetchStatsdHost); - jmxFetchStatsdPort = - getPropertyIntegerValue(properties, JMX_FETCH_STATSD_PORT, parent.jmxFetchStatsdPort); - - healthMetricsEnabled = - getPropertyBooleanValue(properties, HEALTH_METRICS_ENABLED, DEFAULT_METRICS_ENABLED); - healthMetricsStatsdHost = - properties.getProperty(HEALTH_METRICS_STATSD_HOST, parent.healthMetricsStatsdHost); - healthMetricsStatsdPort = - getPropertyIntegerValue( - properties, HEALTH_METRICS_STATSD_PORT, parent.healthMetricsStatsdPort); - - logsInjectionEnabled = - getBooleanSettingFromEnvironment(LOGS_INJECTION_ENABLED, DEFAULT_LOGS_INJECTION_ENABLED); - reportHostName = - getPropertyBooleanValue(properties, TRACE_REPORT_HOSTNAME, parent.reportHostName); - - traceAnnotations = properties.getProperty(TRACE_ANNOTATIONS, parent.traceAnnotations); - - traceMethods = properties.getProperty(TRACE_METHODS, parent.traceMethods); - - traceExecutorsAll = - getPropertyBooleanValue(properties, TRACE_EXECUTORS_ALL, parent.traceExecutorsAll); - traceExecutors = getPropertyListValue(properties, TRACE_EXECUTORS, parent.traceExecutors); - - traceAnalyticsEnabled = - getPropertyBooleanValue(properties, TRACE_ANALYTICS_ENABLED, parent.traceAnalyticsEnabled); - - traceSamplingServiceRules = - getPropertyMapValue( - properties, TRACE_SAMPLING_SERVICE_RULES, parent.traceSamplingServiceRules); - traceSamplingOperationRules = - getPropertyMapValue( - properties, TRACE_SAMPLING_OPERATION_RULES, parent.traceSamplingOperationRules); - traceSampleRate = getPropertyDoubleValue(properties, TRACE_SAMPLE_RATE, parent.traceSampleRate); - traceRateLimit = getPropertyDoubleValue(properties, TRACE_RATE_LIMIT, parent.traceRateLimit); - - profilingEnabled = - getPropertyBooleanValue(properties, PROFILING_ENABLED, parent.profilingEnabled); - profilingUrl = properties.getProperty(PROFILING_URL, parent.profilingUrl); - profilingTags = getPropertyMapValue(properties, PROFILING_TAGS, parent.profilingTags); - profilingStartDelay = - getPropertyIntegerValue(properties, PROFILING_START_DELAY, parent.profilingStartDelay); - profilingStartForceFirst = - getPropertyBooleanValue( - properties, PROFILING_START_FORCE_FIRST, parent.profilingStartForceFirst); - profilingUploadPeriod = - getPropertyIntegerValue(properties, PROFILING_UPLOAD_PERIOD, parent.profilingUploadPeriod); - profilingTemplateOverrideFile = - properties.getProperty( - PROFILING_TEMPLATE_OVERRIDE_FILE, parent.profilingTemplateOverrideFile); - profilingUploadTimeout = - getPropertyIntegerValue( - properties, PROFILING_UPLOAD_TIMEOUT, parent.profilingUploadTimeout); - profilingUploadCompression = - properties.getProperty(PROFILING_UPLOAD_COMPRESSION, parent.profilingUploadCompression); - profilingProxyHost = properties.getProperty(PROFILING_PROXY_HOST, parent.profilingProxyHost); - profilingProxyPort = - getPropertyIntegerValue(properties, PROFILING_PROXY_PORT, parent.profilingProxyPort); - profilingProxyUsername = - properties.getProperty(PROFILING_PROXY_USERNAME, parent.profilingProxyUsername); - profilingProxyPassword = - properties.getProperty(PROFILING_PROXY_PASSWORD, parent.profilingProxyPassword); - - profilingExceptionSampleLimit = - getPropertyIntegerValue( - properties, PROFILING_EXCEPTION_SAMPLE_LIMIT, parent.profilingExceptionSampleLimit); - - profilingExceptionHistogramTopItems = - getPropertyIntegerValue( - properties, - PROFILING_EXCEPTION_HISTOGRAM_TOP_ITEMS, - parent.profilingExceptionHistogramTopItems); - profilingExceptionHistogramMaxCollectionSize = - getPropertyIntegerValue( - properties, - PROFILING_EXCEPTION_HISTOGRAM_MAX_COLLECTION_SIZE, - parent.profilingExceptionHistogramMaxCollectionSize); - - } - - /** - * @return A map of tags to be applied only to the local application root span. - */ - public Map getLocalRootSpanTags() { - final Map runtimeTags = getRuntimeTags(); - final Map result = new HashMap<>(runtimeTags); - result.put(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE); - - if (reportHostName) { - final String hostName = getHostName(); - if (null != hostName && !hostName.isEmpty()) { - result.put(INTERNAL_HOST_NAME, hostName); - } - } - - return Collections.unmodifiableMap(result); - } - - public Map getMergedSpanTags() { - // Do not include runtimeId into span tags: we only want that added to the root span - final Map result = newHashMap(getGlobalTags().size() + spanTags.size()); - result.putAll(getGlobalTags()); - result.putAll(spanTags); - return Collections.unmodifiableMap(result); - } - - public Map getMergedJmxTags() { - final Map runtimeTags = getRuntimeTags(); - final Map result = - newHashMap( - getGlobalTags().size() + jmxTags.size() + runtimeTags.size() + 1 /* for serviceName */); - result.putAll(getGlobalTags()); - result.putAll(jmxTags); - result.putAll(runtimeTags); - // service name set here instead of getRuntimeTags because apm already manages the service tag - // and may chose to override it. - // Additionally, infra/JMX metrics require `service` rather than APM's `service.name` tag - result.put(SERVICE_TAG, serviceName); - return Collections.unmodifiableMap(result); - } - - public Map getMergedProfilingTags() { - final Map runtimeTags = getRuntimeTags(); - final String host = getHostName(); - final Map result = - newHashMap( - getGlobalTags().size() - + profilingTags.size() - + runtimeTags.size() - + 3 /* for serviceName and host and language */); - result.put(HOST_TAG, host); // Host goes first to allow to override it - result.putAll(getGlobalTags()); - result.putAll(profilingTags); - result.putAll(runtimeTags); - // service name set here instead of getRuntimeTags because apm already manages the service tag - // and may chose to override it. - result.put(SERVICE_TAG, serviceName); - result.put(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE); - return Collections.unmodifiableMap(result); - } - - /** - * Returns the sample rate for the specified instrumentation or {@link - * #DEFAULT_ANALYTICS_SAMPLE_RATE} if none specified. - */ - public float getInstrumentationAnalyticsSampleRate(final String... aliases) { - for (final String alias : aliases) { - final Float rate = getFloatSettingFromEnvironment(alias + ".analytics.sample-rate", null); - if (null != rate) { - return rate; - } - } - return DEFAULT_ANALYTICS_SAMPLE_RATE; - } - - /** - * Provide 'global' tags, i.e. tags set everywhere. We have to support old (dd.trace.global.tags) - * version of this setting if new (dd.tags) version has not been specified. - */ - private Map getGlobalTags() { - return tags; - } - - /** - * Return a map of tags required by the datadog backend to link runtime metrics (i.e. jmx) and - * traces. - * - *

These tags must be applied to every runtime metrics and placed on the root span of every - * trace. - * - * @return A map of tag-name -> tag-value - */ - private Map getRuntimeTags() { - final Map result = newHashMap(2); - result.put(RUNTIME_ID_TAG, runtimeId); - return Collections.unmodifiableMap(result); - } - - public String getFinalProfilingUrl() { - if (profilingUrl == null) { - return String.format(PROFILING_URL_TEMPLATE, site); - } else { - return profilingUrl; - } - } - - public boolean isIntegrationEnabled( - final SortedSet integrationNames, final boolean defaultEnabled) { - return integrationEnabled(integrationNames, defaultEnabled); - } - - /** - * @param integrationNames - * @param defaultEnabled - * @return - * @deprecated This method should only be used internally. Use the instance getter instead {@link - * #isIntegrationEnabled(SortedSet, boolean)}. - */ - @Deprecated - private static boolean integrationEnabled( - final SortedSet integrationNames, final boolean defaultEnabled) { - // If default is enabled, we want to enable individually, - // if default is disabled, we want to disable individually. - boolean anyEnabled = defaultEnabled; - for (final String name : integrationNames) { - final boolean configEnabled = - getBooleanSettingFromEnvironment("integration." + name + ".enabled", defaultEnabled); - if (defaultEnabled) { - anyEnabled &= configEnabled; - } else { - anyEnabled |= configEnabled; - } - } - return anyEnabled; - } - - public boolean isJmxFetchIntegrationEnabled( - final SortedSet integrationNames, final boolean defaultEnabled) { - return jmxFetchIntegrationEnabled(integrationNames, defaultEnabled); - } - - public boolean isRuleEnabled(final String name) { - return getBooleanSettingFromEnvironment("trace." + name + ".enabled", true) - && getBooleanSettingFromEnvironment("trace." + name.toLowerCase(Locale.US) + ".enabled", true); - } - - /** - * @param integrationNames - * @param defaultEnabled - * @return - * @deprecated This method should only be used internally. Use the instance getter instead {@link - * #isJmxFetchIntegrationEnabled(SortedSet, boolean)}. - */ - public static boolean jmxFetchIntegrationEnabled( - final SortedSet integrationNames, final boolean defaultEnabled) { - // If default is enabled, we want to enable individually, - // if default is disabled, we want to disable individually. - boolean anyEnabled = defaultEnabled; - for (final String name : integrationNames) { - final boolean configEnabled = - getBooleanSettingFromEnvironment("jmxfetch." + name + ".enabled", defaultEnabled); - if (defaultEnabled) { - anyEnabled &= configEnabled; - } else { - anyEnabled |= configEnabled; - } - } - return anyEnabled; - } - - public boolean isTraceAnalyticsIntegrationEnabled( - final SortedSet integrationNames, final boolean defaultEnabled) { - return traceAnalyticsIntegrationEnabled(integrationNames, defaultEnabled); - } - - /** - * @param integrationNames - * @param defaultEnabled - * @return - * @deprecated This method should only be used internally. Use the instance getter instead {@link - * #isTraceAnalyticsIntegrationEnabled(SortedSet, boolean)}. - */ - public static boolean traceAnalyticsIntegrationEnabled( - final SortedSet integrationNames, final boolean defaultEnabled) { - // If default is enabled, we want to enable individually, - // if default is disabled, we want to disable individually. - boolean anyEnabled = defaultEnabled; - for (final String name : integrationNames) { - final boolean configEnabled = - getBooleanSettingFromEnvironment(name + ".analytics.enabled", defaultEnabled); - if (defaultEnabled) { - anyEnabled &= configEnabled; - } else { - anyEnabled |= configEnabled; - } - } - return anyEnabled; - } - - /** - * Helper method that takes the name, adds a "dd." prefix then checks for System Properties of - * that name. If none found, the name is converted to an Environment Variable and used to check - * the env. If none of the above returns a value, then an optional properties file if checked. If - * setting is not configured in either location, defaultValue is returned. - * - * @param name - * @param defaultValue - * @return - * @deprecated This method should only be used internally. Use the explicit getter instead. - */ - public static String getSettingFromEnvironment(final String name, final String defaultValue) { - String value; - final String systemPropertyName = propertyNameToSystemPropertyName(name); - - // System properties and properties provided from command line have the highest precedence - value = System.getProperties().getProperty(systemPropertyName); - if (null != value) { - return value; - } - - // If value not provided from system properties, looking at env variables - value = System.getenv(propertyNameToEnvironmentVariableName(name)); - if (null != value) { - return value; - } - - // If value is not defined yet, we look at properties optionally defined in a properties file - value = propertiesFromConfigFile.getProperty(systemPropertyName); - if (null != value) { - return value; - } - - return defaultValue; - } - - /** - * @deprecated This method should only be used internally. Use the explicit getter instead. - */ - private static Map getMapSettingFromEnvironment( - final String name, final String defaultValue) { - return parseMap( - getSettingFromEnvironment(name, defaultValue), propertyNameToSystemPropertyName(name)); - } - - /** - * Calls {@link #getSettingFromEnvironment(String, String)} and converts the result to a list by - * splitting on `,`. - * - * @deprecated This method should only be used internally. Use the explicit getter instead. - */ - private static List getListSettingFromEnvironment( - final String name, final String defaultValue) { - return parseList(getSettingFromEnvironment(name, defaultValue)); - } - - /** - * Calls {@link #getSettingFromEnvironment(String, String)} and converts the result to a Boolean. - * - * @deprecated This method should only be used internally. Use the explicit getter instead. - */ - public static Boolean getBooleanSettingFromEnvironment( - final String name, final Boolean defaultValue) { - return getSettingFromEnvironmentWithLog(name, Boolean.class, defaultValue); - } - - /** - * Calls {@link #getSettingFromEnvironment(String, String)} and converts the result to a Float. - * - * @deprecated This method should only be used internally. Use the explicit getter instead. - */ - public static Float getFloatSettingFromEnvironment(final String name, final Float defaultValue) { - return getSettingFromEnvironmentWithLog(name, Float.class, defaultValue); - } - - /** - * Calls {@link #getSettingFromEnvironment(String, String)} and converts the result to a Double. - * - * @deprecated This method should only be used internally. Use the explicit getter instead. - */ - @Deprecated - private static Double getDoubleSettingFromEnvironment( - final String name, final Double defaultValue) { - return getSettingFromEnvironmentWithLog(name, Double.class, defaultValue); - } - - /** - * Calls {@link #getSettingFromEnvironment(String, String)} and converts the result to a Integer. - */ - private static Integer getIntegerSettingFromEnvironment( - final String name, final Integer defaultValue) { - return getSettingFromEnvironmentWithLog(name, Integer.class, defaultValue); - } - - private static T getSettingFromEnvironmentWithLog( - final String name, Class tClass, final T defaultValue) { - try { - return valueOf(getSettingFromEnvironment(name, null), tClass, defaultValue); - } catch (final NumberFormatException e) { - return defaultValue; - } - } - - /** - * Calls {@link #getSettingFromEnvironment(String, String)} and converts the result to a set of - * strings splitting by space or comma. - */ - private static Set getPropagationStyleSetSettingFromEnvironmentOrDefault( - final String name, final String defaultValue) { - final String value = getSettingFromEnvironment(name, defaultValue); - Set result = - convertStringSetToPropagationStyleSet(parseStringIntoSetOfNonEmptyStrings(value)); - - if (result.isEmpty()) { - // Treat empty parsing result as no value and use default instead - result = - convertStringSetToPropagationStyleSet(parseStringIntoSetOfNonEmptyStrings(defaultValue)); - } - - return result; - } - - private static Set getIntegerRangeSettingFromEnvironment( - final String name, final Set defaultValue) { - final String value = getSettingFromEnvironment(name, null); - try { - return value == null ? defaultValue : parseIntegerRangeSet(value, name); - } catch (final NumberFormatException e) { - return defaultValue; - } - } - - /** - * Converts the property name, e.g. 'service.name' into a public environment variable name, e.g. - * `DD_SERVICE_NAME`. - * - * @param setting The setting name, e.g. `service.name` - * @return The public facing environment variable name - */ - private static String propertyNameToEnvironmentVariableName(final String setting) { - return ENV_REPLACEMENT - .matcher(propertyNameToSystemPropertyName(setting).toUpperCase(Locale.US)) - .replaceAll("_"); - } - - /** - * Converts the property name, e.g. 'service.name' into a public system property name, e.g. - * `dd.service.name`. - * - * @param setting The setting name, e.g. `service.name` - * @return The public facing system property name - */ - private static String propertyNameToSystemPropertyName(final String setting) { - return PREFIX + setting; - } - - /** - * @param value to parse by tClass::valueOf - * @param tClass should contain static parsing method "T valueOf(String)" - * @param defaultValue - * @param - * @return value == null || value.trim().isEmpty() ? defaultValue : tClass.valueOf(value) - * @throws NumberFormatException - */ - private static T valueOf( - final String value, final Class tClass, final T defaultValue) { - if (value == null || value.trim().isEmpty()) { - return defaultValue; - } - try { - Method method = tClass.getMethod("valueOf", String.class); - return (T) method.invoke(null, value); - } catch (NumberFormatException e) { - throw e; - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new NumberFormatException(e.toString()); - } catch (Throwable e) { - throw new NumberFormatException(e.toString()); - } - } - - private static Map getPropertyMapValue( - final Properties properties, final String name, final Map defaultValue) { - final String value = properties.getProperty(name); - return value == null || value.trim().isEmpty() ? defaultValue : parseMap(value, name); - } - - private static List getPropertyListValue( - final Properties properties, final String name, final List defaultValue) { - final String value = properties.getProperty(name); - return value == null || value.trim().isEmpty() ? defaultValue : parseList(value); - } - - private static Boolean getPropertyBooleanValue( - final Properties properties, final String name, final Boolean defaultValue) { - return valueOf(properties.getProperty(name), Boolean.class, defaultValue); - } - - private static Integer getPropertyIntegerValue( - final Properties properties, final String name, final Integer defaultValue) { - return valueOf(properties.getProperty(name), Integer.class, defaultValue); - } - - private static Double getPropertyDoubleValue( - final Properties properties, final String name, final Double defaultValue) { - return valueOf(properties.getProperty(name), Double.class, defaultValue); - } - - private static Set getPropagationStyleSetFromPropertyValue( - final Properties properties, final String name) { - final String value = properties.getProperty(name); - if (value != null) { - final Set result = - convertStringSetToPropagationStyleSet(parseStringIntoSetOfNonEmptyStrings(value)); - if (!result.isEmpty()) { - return result; - } - } - // null means parent value should be used - return null; - } - - private static Set getPropertyIntegerRangeValue( - final Properties properties, final String name, final Set defaultValue) { - final String value = properties.getProperty(name); - try { - return value == null ? defaultValue : parseIntegerRangeSet(value, name); - } catch (final NumberFormatException e) { - return defaultValue; - } - } - - private static Map parseMap(final String str, final String settingName) { - // If we ever want to have default values besides an empty map, this will need to change. - if (str == null || str.trim().isEmpty()) { - return Collections.emptyMap(); - } - if (!str.matches("(([^,:]+:[^,:]*,)*([^,:]+:[^,:]*),?)?")) { - return Collections.emptyMap(); - } - - final String[] tokens = str.split(",", -1); - final Map map = newHashMap(tokens.length); - - for (final String token : tokens) { - final String[] keyValue = token.split(":", -1); - if (keyValue.length == 2) { - final String key = keyValue[0].trim(); - final String value = keyValue[1].trim(); - if (value.length() <= 0) { - continue; - } - map.put(key, value); - } - } - return Collections.unmodifiableMap(map); - } - - private static Set parseIntegerRangeSet(String str, final String settingName) - throws NumberFormatException { - str = str.replaceAll("\\s", ""); - if (!str.matches("\\d{3}(?:-\\d{3})?(?:,\\d{3}(?:-\\d{3})?)*")) { - throw new NumberFormatException(); - } - - final String[] tokens = str.split(",", -1); - final Set set = new HashSet<>(); - - for (final String token : tokens) { - final String[] range = token.split("-", -1); - if (range.length == 1) { - set.add(Integer.parseInt(range[0])); - } else if (range.length == 2) { - final int left = Integer.parseInt(range[0]); - final int right = Integer.parseInt(range[1]); - final int min = Math.min(left, right); - final int max = Math.max(left, right); - for (int i = min; i <= max; i++) { - set.add(i); - } - } - } - return Collections.unmodifiableSet(set); - } - - private static Map newHashMap(final int size) { - return new HashMap<>(size + 1, 1f); - } - - /** - * @param map - * @param propNames - * @return new unmodifiable copy of {@param map} where properties are overwritten from environment - */ - private static Map getMapWithPropertiesDefinedByEnvironment( - final Map map, final String... propNames) { - final Map res = new HashMap<>(map); - for (final String propName : propNames) { - final String val = getSettingFromEnvironment(propName, null); - if (val != null) { - res.put(propName, val); - } - } - return Collections.unmodifiableMap(res); - } - - /** - * same as {@link Config#getMapWithPropertiesDefinedByEnvironment(Map, String...)} but using - * {@code properties} as source of values to overwrite inside map - * - * @param map - * @param properties - * @param keys - * @return - */ - private static Map overwriteKeysFromProperties( - final Map map, - final Properties properties, - final String... keys) { - final Map res = new HashMap<>(map); - for (final String propName : keys) { - final String val = properties.getProperty(propName, null); - if (val != null) { - res.put(propName, val); - } - } - return Collections.unmodifiableMap(res); - } - - private static List parseList(final String str) { - if (str == null || str.trim().isEmpty()) { - return Collections.emptyList(); - } - - final String[] tokens = str.split(",", -1); - // Remove whitespace from each item. - for (int i = 0; i < tokens.length; i++) { - tokens[i] = tokens[i].trim(); - } - return Collections.unmodifiableList(Arrays.asList(tokens)); - } - - private static Set parseStringIntoSetOfNonEmptyStrings(final String str) { - // Using LinkedHashSet to preserve original string order - final Set result = new LinkedHashSet<>(); - // Java returns single value when splitting an empty string. We do not need that value, so - // we need to throw it out. - for (final String value : str.split(SPLIT_BY_SPACE_OR_COMMA_REGEX)) { - if (!value.isEmpty()) { - result.add(value); - } - } - return Collections.unmodifiableSet(result); - } - - private static Set convertStringSetToPropagationStyleSet( - final Set input) { - // Using LinkedHashSet to preserve original string order - final Set result = new LinkedHashSet<>(); - for (final String value : input) { - try { - result.add(PropagationStyle.valueOf(value.toUpperCase(Locale.US))); - } catch (final IllegalArgumentException e) { - } - } - return Collections.unmodifiableSet(result); - } - - /** - * Loads the optional configuration properties file into the global {@link Properties} object. - * - * @return The {@link Properties} object. the returned instance might be empty of file does not - * exist or if it is in a wrong format. - */ - private static Properties loadConfigurationFile() { - final Properties properties = new Properties(); - - // Reading from system property first and from env after - String configurationFilePath = - System.getProperty(propertyNameToSystemPropertyName(CONFIGURATION_FILE)); - if (null == configurationFilePath) { - configurationFilePath = - System.getenv(propertyNameToEnvironmentVariableName(CONFIGURATION_FILE)); - } - if (null == configurationFilePath) { - return properties; - } - - // Normalizing tilde (~) paths for unix systems - configurationFilePath = - configurationFilePath.replaceFirst("^~", System.getProperty("user.home")); - - // Configuration properties file is optional - final File configurationFile = new File(configurationFilePath); - if (!configurationFile.exists()) { - return properties; - } - - try (final FileReader fileReader = new FileReader(configurationFile)) { - properties.load(fileReader); - } catch (final FileNotFoundException fnf) { - } catch (final IOException ioe) { - } - - return properties; - } - - /** - * Returns the detected hostname. First tries locally, then using DNS - */ - private static String getHostName() { - String possibleHostname; - - // Try environment variable. This works in almost all environments - if (System.getProperty("os.name").startsWith("Windows")) { - possibleHostname = System.getenv("COMPUTERNAME"); - } else { - possibleHostname = System.getenv("HOSTNAME"); - } - - if (possibleHostname != null && !possibleHostname.isEmpty()) { - return possibleHostname.trim(); - } - - // Try hostname command - try (final BufferedReader reader = - new BufferedReader( - new InputStreamReader(Runtime.getRuntime().exec("hostname").getInputStream()))) { - possibleHostname = reader.readLine(); - } catch (final Exception ignore) { - // Ignore. Hostname command is not always available - } - - if (possibleHostname != null && !possibleHostname.isEmpty()) { - return possibleHostname.trim(); - } - - // From DNS - try { - return InetAddress.getLocalHost().getHostName(); - } catch (final UnknownHostException e) { - // If we are not able to detect the hostname we do not throw an exception. - } - - return null; - } - - // This has to be placed after all other static fields to give them a chance to initialize - private static final Config INSTANCE = new Config(); - - public static Config get() { - return INSTANCE; - } - - public static Config get(final Properties properties) { - if (properties == null || properties.isEmpty()) { - return INSTANCE; - } else { - return new Config(properties, INSTANCE); - } - } - - // region GENERATED GETTERS - - public String getRuntimeId() { - return runtimeId; - } - - public String getSite() { - return site; - } - - public String getServiceName() { - return serviceName; - } - - public boolean isTraceEnabled() { - return traceEnabled; - } - - public boolean isIntegrationsEnabled() { - return integrationsEnabled; - } - - public String getWriterType() { - return writerType; - } - - public String getAgentHost() { - return agentHost; - } - - public int getAgentPort() { - return agentPort; - } - - public String getAgentUnixDomainSocket() { - return agentUnixDomainSocket; - } - - public boolean isPrioritySamplingEnabled() { - return prioritySamplingEnabled; - } - - public boolean isTraceResolverEnabled() { - return traceResolverEnabled; - } - - public Map getServiceMapping() { - return serviceMapping; - } - - public List getExcludedClasses() { - return excludedClasses; - } - - public Map getHeaderTags() { - return headerTags; - } - - public Set getHttpServerErrorStatuses() { - return httpServerErrorStatuses; - } - - public Set getHttpClientErrorStatuses() { - return httpClientErrorStatuses; - } - - public boolean isHttpServerTagQueryString() { - return httpServerTagQueryString; - } - - public boolean isHttpClientTagQueryString() { - return httpClientTagQueryString; - } - - public boolean isHttpClientSplitByDomain() { - return httpClientSplitByDomain; - } - - public boolean isDbClientSplitByInstance() { - return dbClientSplitByInstance; - } - - public Set getSplitByTags() { - return splitByTags; - } - - public Integer getScopeDepthLimit() { - return scopeDepthLimit; - } - - public Integer getPartialFlushMinSpans() { - return partialFlushMinSpans; - } - - public boolean isRuntimeContextFieldInjection() { - return runtimeContextFieldInjection; - } - - public Set getPropagationStylesToExtract() { - return propagationStylesToExtract; - } - - public Set getPropagationStylesToInject() { - return propagationStylesToInject; - } - - public boolean isJmxFetchEnabled() { - return jmxFetchEnabled; - } - - public String getJmxFetchConfigDir() { - return jmxFetchConfigDir; - } - - public List getJmxFetchConfigs() { - return jmxFetchConfigs; - } - - public List getJmxFetchMetricsConfigs() { - return jmxFetchMetricsConfigs; - } - - public Integer getJmxFetchCheckPeriod() { - return jmxFetchCheckPeriod; - } - - public Integer getJmxFetchRefreshBeansPeriod() { - return jmxFetchRefreshBeansPeriod; - } - - public String getJmxFetchStatsdHost() { - return jmxFetchStatsdHost; - } - - public Integer getJmxFetchStatsdPort() { - return jmxFetchStatsdPort; - } - - public boolean isHealthMetricsEnabled() { - return healthMetricsEnabled; - } - - public String getHealthMetricsStatsdHost() { - return healthMetricsStatsdHost; - } - - public Integer getHealthMetricsStatsdPort() { - return healthMetricsStatsdPort; - } - - public boolean isLogsInjectionEnabled() { - return logsInjectionEnabled; - } - - public boolean isReportHostName() { - return reportHostName; - } - - public String getTraceAnnotations() { - return traceAnnotations; - } - - public String getTraceMethods() { - return traceMethods; - } - - public boolean isTraceExecutorsAll() { - return traceExecutorsAll; - } - - public List getTraceExecutors() { - return traceExecutors; - } - - public boolean isTraceAnalyticsEnabled() { - return traceAnalyticsEnabled; - } - - public Map getTraceSamplingServiceRules() { - return traceSamplingServiceRules; - } - - public Map getTraceSamplingOperationRules() { - return traceSamplingOperationRules; - } - - public Double getTraceSampleRate() { - return traceSampleRate; - } - - public Double getTraceRateLimit() { - return traceRateLimit; - } - - public boolean isProfilingEnabled() { - return profilingEnabled; - } - - public int getProfilingStartDelay() { - return profilingStartDelay; - } - - public boolean isProfilingStartForceFirst() { - return profilingStartForceFirst; - } - - public int getProfilingUploadPeriod() { - return profilingUploadPeriod; - } - - public String getProfilingTemplateOverrideFile() { - return profilingTemplateOverrideFile; - } - - public int getProfilingUploadTimeout() { - return profilingUploadTimeout; - } - - public String getProfilingUploadCompression() { - return profilingUploadCompression; - } - - public String getProfilingProxyHost() { - return profilingProxyHost; - } - - public int getProfilingProxyPort() { - return profilingProxyPort; - } - - public String getProfilingProxyUsername() { - return profilingProxyUsername; - } - - public String getProfilingProxyPassword() { - return profilingProxyPassword; - } - - public int getProfilingExceptionSampleLimit() { - return profilingExceptionSampleLimit; - } - - public int getProfilingExceptionHistogramTopItems() { - return profilingExceptionHistogramTopItems; - } - - public int getProfilingExceptionHistogramMaxCollectionSize() { - return profilingExceptionHistogramMaxCollectionSize; - } - - // endregion - - // region GENERATED toString() - - @Override - public String toString() { - return "Config{" + - "runtimeId='" + runtimeId + '\'' + - ", site='" + site + '\'' + - ", serviceName='" + serviceName + '\'' + - ", traceEnabled=" + traceEnabled + - ", integrationsEnabled=" + integrationsEnabled + - ", writerType='" + writerType + '\'' + - ", agentHost='" + agentHost + '\'' + - ", agentPort=" + agentPort + - ", agentUnixDomainSocket='" + agentUnixDomainSocket + '\'' + - ", prioritySamplingEnabled=" + prioritySamplingEnabled + - ", traceResolverEnabled=" + traceResolverEnabled + - ", serviceMapping=" + serviceMapping + - ", tags=" + tags + - ", spanTags=" + spanTags + - ", jmxTags=" + jmxTags + - ", excludedClasses=" + excludedClasses + - ", headerTags=" + headerTags + - ", httpServerErrorStatuses=" + httpServerErrorStatuses + - ", httpClientErrorStatuses=" + httpClientErrorStatuses + - ", httpServerTagQueryString=" + httpServerTagQueryString + - ", httpClientTagQueryString=" + httpClientTagQueryString + - ", httpClientSplitByDomain=" + httpClientSplitByDomain + - ", dbClientSplitByInstance=" + dbClientSplitByInstance + - ", splitByTags=" + splitByTags + - ", scopeDepthLimit=" + scopeDepthLimit + - ", partialFlushMinSpans=" + partialFlushMinSpans + - ", runtimeContextFieldInjection=" + runtimeContextFieldInjection + - ", propagationStylesToExtract=" + propagationStylesToExtract + - ", propagationStylesToInject=" + propagationStylesToInject + - ", jmxFetchEnabled=" + jmxFetchEnabled + - ", jmxFetchConfigDir='" + jmxFetchConfigDir + '\'' + - ", jmxFetchConfigs=" + jmxFetchConfigs + - ", jmxFetchMetricsConfigs=" + jmxFetchMetricsConfigs + - ", jmxFetchCheckPeriod=" + jmxFetchCheckPeriod + - ", jmxFetchRefreshBeansPeriod=" + jmxFetchRefreshBeansPeriod + - ", jmxFetchStatsdHost='" + jmxFetchStatsdHost + '\'' + - ", jmxFetchStatsdPort=" + jmxFetchStatsdPort + - ", healthMetricsEnabled=" + healthMetricsEnabled + - ", healthMetricsStatsdHost='" + healthMetricsStatsdHost + '\'' + - ", healthMetricsStatsdPort=" + healthMetricsStatsdPort + - ", logsInjectionEnabled=" + logsInjectionEnabled + - ", reportHostName=" + reportHostName + - ", traceAnnotations='" + traceAnnotations + '\'' + - ", traceMethods='" + traceMethods + '\'' + - ", traceExecutorsAll=" + traceExecutorsAll + - ", traceExecutors=" + traceExecutors + - ", traceAnalyticsEnabled=" + traceAnalyticsEnabled + - ", traceSamplingServiceRules=" + traceSamplingServiceRules + - ", traceSamplingOperationRules=" + traceSamplingOperationRules + - ", traceSampleRate=" + traceSampleRate + - ", traceRateLimit=" + traceRateLimit + - ", profilingEnabled=" + profilingEnabled + - ", profilingUrl='" + profilingUrl + '\'' + - ", profilingTags=" + profilingTags + - ", profilingStartDelay=" + profilingStartDelay + - ", profilingStartForceFirst=" + profilingStartForceFirst + - ", profilingUploadPeriod=" + profilingUploadPeriod + - ", profilingTemplateOverrideFile='" + profilingTemplateOverrideFile + '\'' + - ", profilingUploadTimeout=" + profilingUploadTimeout + - ", profilingUploadCompression='" + profilingUploadCompression + '\'' + - ", profilingProxyHost='" + profilingProxyHost + '\'' + - ", profilingProxyPort=" + profilingProxyPort + - ", profilingProxyUsername='" + profilingProxyUsername + '\'' + - ", profilingProxyPassword='" + profilingProxyPassword + '\'' + - ", profilingExceptionSampleLimit=" + profilingExceptionSampleLimit + - ", profilingExceptionHistogramTopItems=" + profilingExceptionHistogramTopItems + - ", profilingExceptionHistogramMaxCollectionSize=" + profilingExceptionHistogramMaxCollectionSize + - '}'; - } - - - // endregion - -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/DDSpanTypes.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/DDSpanTypes.java deleted file mode 100644 index d4689fc07a..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/DDSpanTypes.java +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api; - -public class DDSpanTypes { - public static final String HTTP_CLIENT = "http"; - public static final String HTTP_SERVER = "web"; - @Deprecated public static final String WEB_SERVLET = HTTP_SERVER; - public static final String RPC = "rpc"; - public static final String CACHE = "cache"; - - public static final String SQL = "sql"; - public static final String MONGO = "mongodb"; - public static final String CASSANDRA = "cassandra"; - public static final String COUCHBASE = "db"; // Using generic for now. - public static final String REDIS = "redis"; - public static final String MEMCACHED = "memcached"; - public static final String ELASTICSEARCH = "elasticsearch"; - public static final String HIBERNATE = "hibernate"; - - public static final String MESSAGE_CLIENT = "queue"; - public static final String MESSAGE_CONSUMER = "queue"; - public static final String MESSAGE_PRODUCER = "queue"; -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/DDTags.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/DDTags.java deleted file mode 100644 index 7732f16aa7..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/DDTags.java +++ /dev/null @@ -1,33 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api; - -public class DDTags { - public static final String SPAN_TYPE = "span.type"; - public static final String SERVICE_NAME = "service.name"; - public static final String RESOURCE_NAME = "resource.name"; - public static final String THREAD_NAME = "thread.name"; - public static final String THREAD_ID = "thread.id"; - public static final String DB_STATEMENT = "sql.query"; - - public static final String HTTP_QUERY = "http.query.string"; - public static final String HTTP_FRAGMENT = "http.fragment.string"; - - public static final String USER_NAME = "user.principal"; - - public static final String ERROR_MSG = "error.msg"; // string representing the error message - public static final String ERROR_TYPE = "error.type"; // string representing the type of the error - public static final String ERROR_STACK = "error.stack"; // human readable version of the stack - - public static final String ANALYTICS_SAMPLE_RATE = "_dd1.sr.eausr"; - @Deprecated public static final String EVENT_SAMPLE_RATE = ANALYTICS_SAMPLE_RATE; - - /** Manually force tracer to be keep the trace */ - public static final String MANUAL_KEEP = "manual.keep"; - /** Manually force tracer to be drop the trace */ - public static final String MANUAL_DROP = "manual.drop"; -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/DDTraceApiInfo.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/DDTraceApiInfo.java deleted file mode 100644 index 8814f708fa..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/DDTraceApiInfo.java +++ /dev/null @@ -1,35 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api; - -import java.io.BufferedReader; -import java.io.InputStreamReader; - -public class DDTraceApiInfo { - public static final String VERSION; - - static { - String v; - try (final BufferedReader br = - new BufferedReader( - new InputStreamReader( - DDTraceApiInfo.class.getResourceAsStream("/dd-trace-api.version"), "UTF-8"))) { - final StringBuilder sb = new StringBuilder(); - - for (int c = br.read(); c != -1; c = br.read()) sb.append((char) c); - - v = sb.toString().trim(); - } catch (final Exception e) { - v = "unknown"; - } - VERSION = v; - } - - public static void main(final String... args) { - System.out.println(VERSION); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/Trace.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/Trace.java deleted file mode 100644 index bcfbcd29c9..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/Trace.java +++ /dev/null @@ -1,25 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** Set this annotation to a method so the dd-java-agent considers it for tracing. */ -@Retention(RUNTIME) -@Target(METHOD) -public @interface Trace { - - /** The operation name to set. By default it takes the method's name */ - String operationName() default ""; - - /** The resource name. By default it uses the same value as the operation name */ - String resourceName() default ""; -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/Tracer.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/Tracer.java deleted file mode 100644 index d8254b23e6..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/Tracer.java +++ /dev/null @@ -1,38 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api; - -import com.datadog.trace.api.interceptor.TraceInterceptor; -import com.datadog.trace.context.ScopeListener; - -/** A class with Datadog tracer features. */ -public interface Tracer { - - /** Get the trace id of the active trace. Returns 0 if there is no active trace. */ - String getTraceId(); - - /** - * Get the span id of the active span of the active trace. Returns 0 if there is no active trace. - */ - String getSpanId(); - - /** - * Add a new interceptor to the tracer. Interceptors with duplicate priority to existing ones are - * ignored. - * - * @param traceInterceptor - * @return false if an interceptor with same priority exists. - */ - boolean addTraceInterceptor(TraceInterceptor traceInterceptor); - - /** - * Attach a scope listener to the global scope manager - * - * @param listener listener to attach - */ - void addScopeListener(ScopeListener listener); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/interceptor/MutableSpan.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/interceptor/MutableSpan.java deleted file mode 100644 index c8f5da18ad..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/interceptor/MutableSpan.java +++ /dev/null @@ -1,73 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api.interceptor; - -import com.datadog.trace.api.DDTags; - -import java.util.Map; - -public interface MutableSpan { - - /** @return Start time with nanosecond scale, but millisecond resolution. */ - long getStartTime(); - - /** @return Duration with nanosecond scale. */ - long getDurationNano(); - - String getOperationName(); - - MutableSpan setOperationName(final String serviceName); - - String getServiceName(); - - MutableSpan setServiceName(final String serviceName); - - String getResourceName(); - - MutableSpan setResourceName(final String resourceName); - - Integer getSamplingPriority(); - - /** - * @deprecated Use {@link io.opentracing.Span#setTag(String, boolean)} instead using either tag - * names {@link DDTags#MANUAL_KEEP} or {@link - * DDTags#MANUAL_DROP}. - * @param newPriority - * @return - */ - @Deprecated - MutableSpan setSamplingPriority(final int newPriority); - - String getSpanType(); - - MutableSpan setSpanType(final String type); - - Map getTags(); - - MutableSpan setTag(final String tag, final String value); - - MutableSpan setTag(final String tag, final boolean value); - - MutableSpan setTag(final String tag, final Number value); - - Boolean isError(); - - MutableSpan setError(boolean value); - - /** @deprecated Use {@link #getLocalRootSpan()} instead. */ - @Deprecated - MutableSpan getRootSpan(); - - /** - * Returns the root span for current the trace fragment. In the context of distributed tracing - * this method returns the root span only for the fragment generated by the currently traced - * application. - * - * @return The root span for the current trace fragment. - */ - MutableSpan getLocalRootSpan(); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/interceptor/TraceInterceptor.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/interceptor/TraceInterceptor.java deleted file mode 100644 index 122fdab98e..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/interceptor/TraceInterceptor.java +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api.interceptor; - -import java.util.Collection; - -public interface TraceInterceptor { - - /** - * After a trace is "complete" but before it is written, it is provided to the interceptors to - * modify. The result following all interceptors is sampled then sent to the trace writer. - * - * @param trace - The collection of spans that represent a trace. Can be modified in place. Order - * of spans should not be relied upon. - * @return A potentially modified or replaced collection of spans. Must not be null. - */ - Collection onTraceComplete(Collection trace); - - /** - * @return A unique priority for sorting relative to other TraceInterceptors. Unique because - * interceptors are stored in a sorted set, so duplicates will not be added. - */ - int priority(); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/api/sampling/PrioritySampling.java b/dd-sdk-android/src/main/java/com/datadog/trace/api/sampling/PrioritySampling.java deleted file mode 100644 index 22c681bf89..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/api/sampling/PrioritySampling.java +++ /dev/null @@ -1,26 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.api.sampling; - -public class PrioritySampling { - /** - * Implementation detail of the client. will not be sent to the agent or propagated. - * - *

Internal value used when the priority sampling flag has not been set on the span context. - */ - public static final int UNSET = Integer.MIN_VALUE; - /** The sampler has decided to drop the trace. */ - public static final int SAMPLER_DROP = 0; - /** The sampler has decided to keep the trace. */ - public static final int SAMPLER_KEEP = 1; - /** The user has decided to drop the trace. */ - public static final int USER_DROP = -1; - /** The user has decided to keep the trace. */ - public static final int USER_KEEP = 2; - - private PrioritySampling() {} -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/AbstractSampler.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/AbstractSampler.java deleted file mode 100644 index cfffa4b2f1..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/AbstractSampler.java +++ /dev/null @@ -1,51 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -import com.datadog.opentracing.DDSpan; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Pattern; - -@Deprecated -public abstract class AbstractSampler implements Sampler { - - /** Sample tags */ - protected Map skipTagsPatterns = new HashMap<>(); - - @Override - public boolean sample(final DDSpan span) { - - // Filter by tag values - for (final Entry entry : skipTagsPatterns.entrySet()) { - final Object value = span.getTags().get(entry.getKey()); - if (value != null) { - final String strValue = String.valueOf(value); - final Pattern skipPattern = entry.getValue(); - if (skipPattern.matcher(strValue).matches()) { - return false; - } - } - } - - return doSample(span); - } - - /** - * Pattern based skipping of tag values - * - * @param tag - * @param skipPattern - */ - @Deprecated - public void addSkipTagPattern(final String tag, final Pattern skipPattern) { - skipTagsPatterns.put(tag, skipPattern); - } - - protected abstract boolean doSample(DDSpan span); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/AllSampler.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/AllSampler.java deleted file mode 100644 index 4cb35158cc..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/AllSampler.java +++ /dev/null @@ -1,23 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -import com.datadog.opentracing.DDSpan; - -/** Sampler that always says yes... */ -public class AllSampler extends AbstractSampler { - - @Override - public boolean doSample(final DDSpan span) { - return true; - } - - @Override - public String toString() { - return "AllSampler { sample=true }"; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/DeterministicSampler.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/DeterministicSampler.java deleted file mode 100644 index ab6568213d..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/DeterministicSampler.java +++ /dev/null @@ -1,52 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -import com.datadog.opentracing.DDSpan; -import com.datadog.opentracing.DDTracer; -import java.math.BigDecimal; -import java.math.BigInteger; - -/** - * This implements the deterministic sampling algorithm used by the Datadog Agent as well as the - * tracers for other languages - */ -public class DeterministicSampler implements RateSampler { - private static final BigInteger KNUTH_FACTOR = new BigInteger("1111111111111111111"); - private static final BigDecimal TRACE_ID_MAX_AS_BIG_DECIMAL = - new BigDecimal(DDTracer.TRACE_ID_MAX); - private static final BigInteger MODULUS = new BigInteger("2").pow(64); - - private final BigInteger cutoff; - private final double rate; - - public DeterministicSampler(final double rate) { - this.rate = rate; - cutoff = new BigDecimal(rate).multiply(TRACE_ID_MAX_AS_BIG_DECIMAL).toBigInteger(); - - } - - @Override - public boolean sample(final DDSpan span) { - final boolean sampled; - if (rate == 1) { - sampled = true; - } else if (rate == 0) { - sampled = false; - } else { - sampled = span.getTraceId().multiply(KNUTH_FACTOR).mod(MODULUS).compareTo(cutoff) < 0; - } - - - return sampled; - } - - @Override - public double getSampleRate() { - return rate; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/PrioritySampler.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/PrioritySampler.java deleted file mode 100644 index 1e99fb0432..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/PrioritySampler.java +++ /dev/null @@ -1,13 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -import com.datadog.opentracing.DDSpan; - -public interface PrioritySampler { - void setSamplingPriority(DDSpan span); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/PrioritySampling.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/PrioritySampling.java deleted file mode 100644 index a2068ebbe1..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/PrioritySampling.java +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -/** @deprecated Replaced by {@link com.datadog.trace.api.sampling.PrioritySampling} . */ -@Deprecated -public class PrioritySampling { - /** - * Implementation detail of the client. will not be sent to the agent or propagated. - * - *

Internal value used when the priority sampling flag has not been set on the span context. - */ - public static final int UNSET = Integer.MIN_VALUE; - /** The sampler has decided to drop the trace. */ - public static final int SAMPLER_DROP = 0; - /** The sampler has decided to keep the trace. */ - public static final int SAMPLER_KEEP = 1; - /** The user has decided to drop the trace. */ - public static final int USER_DROP = -1; - /** The user has decided to keep the trace. */ - public static final int USER_KEEP = 2; - - private PrioritySampling() {} -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/RateByServiceSampler.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/RateByServiceSampler.java deleted file mode 100644 index 62f71a23a0..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/RateByServiceSampler.java +++ /dev/null @@ -1,84 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -import static java.util.Collections.singletonMap; -import static java.util.Collections.unmodifiableMap; - -import com.datadog.opentracing.DDSpan; -import com.datadog.trace.api.sampling.PrioritySampling; -import java.util.Map; - -/** - * A rate sampler which maintains different sample rates per service+env name. - * - *

The configuration of (serviceName,env)->rate is configured by the core agent. - */ -public class RateByServiceSampler implements Sampler, PrioritySampler { - public static final String SAMPLING_AGENT_RATE = "_dd.agent_psr"; - - /** Key for setting the default/baseline rate */ - private static final String DEFAULT_KEY = "service:,env:"; - - private static final double DEFAULT_RATE = 1.0; - - private volatile Map serviceRates = - unmodifiableMap(singletonMap(DEFAULT_KEY, createRateSampler(DEFAULT_RATE))); - - @Override - public boolean sample(final DDSpan span) { - // Priority sampling sends all traces to the core agent, including traces marked dropped. - // This allows the core agent to collect stats on all traces. - return true; - } - - /** If span is a root span, set the span context samplingPriority to keep or drop */ - @Override - public void setSamplingPriority(final DDSpan span) { - final String serviceName = span.getServiceName(); - final String env = getSpanEnv(span); - final String key = "service:" + serviceName + ",env:" + env; - - final Map rates = serviceRates; - RateSampler sampler = serviceRates.get(key); - if (sampler == null) { - sampler = rates.get(DEFAULT_KEY); - } - - final boolean priorityWasSet; - - if (sampler.sample(span)) { - priorityWasSet = span.context().setSamplingPriority(PrioritySampling.SAMPLER_KEEP); - } else { - priorityWasSet = span.context().setSamplingPriority(PrioritySampling.SAMPLER_DROP); - } - - // Only set metrics if we actually set the sampling priority - // We don't know until the call is completed because the lock is internal to DDSpanContext - if (priorityWasSet) { - span.context().setMetric(SAMPLING_AGENT_RATE, sampler.getSampleRate()); - } - } - - private static String getSpanEnv(final DDSpan span) { - return null == span.getTags().get("env") ? "" : String.valueOf(span.getTags().get("env")); - } - - - private RateSampler createRateSampler(final double sampleRate) { - final double sanitizedRate; - if (sampleRate < 0) { - sanitizedRate = 1; - } else if (sampleRate > 1) { - sanitizedRate = 1; - } else { - sanitizedRate = sampleRate; - } - - return new DeterministicSampler(sanitizedRate); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/RateSampler.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/RateSampler.java deleted file mode 100644 index 44659ff5c2..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/RateSampler.java +++ /dev/null @@ -1,11 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -public interface RateSampler extends Sampler { - double getSampleRate(); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/Sampler.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/Sampler.java deleted file mode 100644 index 3a02df06e8..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/Sampler.java +++ /dev/null @@ -1,47 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -import com.datadog.opentracing.DDSpan; -import com.datadog.trace.api.Config; - -import java.util.Properties; - -/** Main interface to sample a collection of traces. */ -public interface Sampler { - - /** - * Sample a collection of traces based on the parent span - * - * @param span the parent span with its context - * @return true when the trace/spans has to be reported/written - */ - boolean sample(DDSpan span); - - final class Builder { - public static Sampler forConfig(final Config config) { - Sampler sampler; - if (config != null) { - - if (config.isPrioritySamplingEnabled()) { - sampler = new RateByServiceSampler(); - } else { - sampler = new AllSampler(); - } - } else { - sampler = new AllSampler(); - } - return sampler; - } - - public static Sampler forConfig(final Properties config) { - return forConfig(Config.get(config)); - } - - private Builder() {} - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/SamplingRule.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/SamplingRule.java deleted file mode 100644 index 356f18259c..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/sampling/SamplingRule.java +++ /dev/null @@ -1,79 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.sampling; - -import com.datadog.opentracing.DDSpan; -import java.util.regex.Pattern; - -public abstract class SamplingRule { - private final RateSampler sampler; - - public SamplingRule(final RateSampler sampler) { - this.sampler = sampler; - } - - public abstract boolean matches(DDSpan span); - - public boolean sample(final DDSpan span) { - return sampler.sample(span); - } - - public RateSampler getSampler() { - return sampler; - } - - public static class AlwaysMatchesSamplingRule extends SamplingRule { - - public AlwaysMatchesSamplingRule(final RateSampler sampler) { - super(sampler); - } - - @Override - public boolean matches(final DDSpan span) { - return true; - } - } - - public abstract static class PatternMatchSamplingRule extends SamplingRule { - private final Pattern pattern; - - public PatternMatchSamplingRule(final String regex, final RateSampler sampler) { - super(sampler); - this.pattern = Pattern.compile(regex); - } - - @Override - public boolean matches(final DDSpan span) { - final String relevantString = getRelevantString(span); - return relevantString != null && pattern.matcher(relevantString).matches(); - } - - protected abstract String getRelevantString(DDSpan span); - } - - public static class ServiceSamplingRule extends PatternMatchSamplingRule { - public ServiceSamplingRule(final String regex, final RateSampler sampler) { - super(regex, sampler); - } - - @Override - protected String getRelevantString(final DDSpan span) { - return span.getServiceName(); - } - } - - public static class OperationSamplingRule extends PatternMatchSamplingRule { - public OperationSamplingRule(final String regex, final RateSampler sampler) { - super(regex, sampler); - } - - @Override - protected String getRelevantString(final DDSpan span) { - return span.getOperationName(); - } - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/util/Clock.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/util/Clock.java deleted file mode 100644 index c1bae860cb..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/util/Clock.java +++ /dev/null @@ -1,59 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.util; - -import java.util.concurrent.TimeUnit; - -/** - * A simple wrapper for system clock that aims to provide the current time - * - *

- * - *

- * - *

- * - *

The JDK provides two clocks: - *

  • one in nanoseconds, for precision, but it can only use to measure durations - *
  • one in milliseconds, for accuracy, useful to provide epoch time - * - *

    - * - *

    At this time, we are using a millis precision (converted to micros) in order to guarantee - * consistency between the span start times and the durations - */ -public class Clock { - - /** - * Get the current nanos ticks, this method can't be use for date accuracy (only duration - * calculations) - * - * @return The current nanos ticks - */ - public static long currentNanoTicks() { - return System.nanoTime(); - } - - /** - * Get the current time in micros. The actual precision is the millis - * - * @return the current epoch time in micros - */ - public static long currentMicroTime() { - return TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); - } - - /** - * Get the current time in nanos. The actual precision is the millis Note: this will overflow in - * ~290 years after epoch - * - * @return the current epoch time in nanos - */ - public static long currentNanoTime() { - return TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/writer/LoggingWriter.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/writer/LoggingWriter.java deleted file mode 100644 index 93cc49cd15..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/writer/LoggingWriter.java +++ /dev/null @@ -1,34 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.writer; - -import com.datadog.opentracing.DDSpan; -import java.util.List; - -public class LoggingWriter implements Writer { - - @Override - public void write(final List trace) { - } - - @Override - public void incrementTraceCount() { - } - - @Override - public void close() { - } - - @Override - public void start() { - } - - @Override - public String toString() { - return "LoggingWriter { }"; - } -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/common/writer/Writer.java b/dd-sdk-android/src/main/java/com/datadog/trace/common/writer/Writer.java deleted file mode 100644 index 2d458a9780..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/common/writer/Writer.java +++ /dev/null @@ -1,36 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.common.writer; - -import com.datadog.opentracing.DDSpan; -import java.io.Closeable; -import java.util.List; - -/** A writer is responsible to send collected spans to some place */ -public interface Writer extends Closeable { - - /** - * Write a trace represented by the entire list of all the finished spans - * - * @param trace the list of spans to write - */ - void write(List trace); - - /** Start the writer */ - void start(); - - /** - * Indicates to the writer that no future writing will come and it should terminates all - * connections and tasks - */ - @Override - void close(); - - /** Count that a trace was captured for stats, but without reporting it. */ - void incrementTraceCount(); - -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/context/ScopeListener.java b/dd-sdk-android/src/main/java/com/datadog/trace/context/ScopeListener.java deleted file mode 100644 index 40c95c609c..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/context/ScopeListener.java +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.context; - -/** Hooks for scope activation */ -public interface ScopeListener { - /** - * Called just after a scope becomes the active scope - * - *

    May be called multiple times. When a scope is initially created, or after a child scope is - * deactivated. - */ - void afterScopeActivated(); - - /** Called just after a scope is closed. */ - void afterScopeClosed(); -} diff --git a/dd-sdk-android/src/main/java/com/datadog/trace/context/TraceScope.java b/dd-sdk-android/src/main/java/com/datadog/trace/context/TraceScope.java deleted file mode 100644 index 1a254a7e1c..0000000000 --- a/dd-sdk-android/src/main/java/com/datadog/trace/context/TraceScope.java +++ /dev/null @@ -1,59 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.trace.context; - -import java.io.Closeable; - -/** An object which can propagate a datadog trace across multiple threads. */ -public interface TraceScope extends Closeable { - /** - * Prevent the trace attached to this TraceScope from reporting until the returned Continuation - * finishes. - * - *

    Should be called on the parent thread. - */ - Continuation capture(); - - /** Close the activated context and allow any underlying spans to finish. */ - @Override - void close(); - - /** If true, this context will propagate across async boundaries. */ - boolean isAsyncPropagating(); - - /** - * Enable or disable async propagation. Async propagation is initially set to false. - * - * @param value The new propagation value. True == propagate. False == don't propagate. - */ - void setAsyncPropagation(boolean value); - - /** Used to pass async context between workers. */ - interface Continuation { - /** - * Activate the continuation. - * - *

    Should be called on the child thread. - */ - TraceScope activate(); - - /** - * Cancel the continuation. This also closes parent scope. - * - *

    FIXME: the fact that this is closing parent scope is confusing, we should review this in - * new API. - */ - void close(); - - /** - * Close the continuation. - * - * @param closeContinuationScope true iff parent scope should also be closed - */ - void close(boolean closeContinuationScope); - } -} diff --git a/dd-sdk-android/src/main/json/_common-schema.json b/dd-sdk-android/src/main/json/_common-schema.json deleted file mode 100644 index c2bb2e33ce..0000000000 --- a/dd-sdk-android/src/main/json/_common-schema.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "_common-schema.json", - "type": "object", - "description": "Schema of common properties of RUM events", - "required": [ - "date", - "application", - "session", - "view", - "_dd" - ], - "properties": { - "date": { - "type": "integer", - "description": "Start of the event in ms from epoch", - "minimum": 0 - }, - "application": { - "type": "object", - "description": "Application properties", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string", - "description": "UUID of the application", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - } - } - }, - "session": { - "type": "object", - "description": "Session properties", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "description": "UUID of the session", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - }, - "type": { - "type": "string", - "description": "Type of the session", - "enum": ["user", "synthetics"] - } - } - }, - "view": { - "type": "object", - "description": "View properties", - "required": [ - "id", - "url" - ], - "properties": { - "id": { - "type": "string", - "description": "UUID of the view", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - }, - "referrer": { - "type": "string", - "description": "URL that linked to the initial view of the page" - }, - "url": { - "type": "string", - "description": "URL of the view" - } - } - }, - "usr": { - "type": "object", - "description": "User properties", - "properties": { - "id": { - "type": "string", - "description": "Identifier of the user" - }, - "name": { - "type": "string", - "description": "Name of the user" - }, - "email": { - "type": "string", - "description": "Email of the user" - } - } - }, - "connectivity": { - "type": "object", - "description": "Device connectivity properties", - "required": [ - "status", - "interfaces" - ], - "properties": { - "status": { - "type": "string", - "description": "Status of the device connectivity", - "enum": ["connected", "not_connected", "maybe"] - }, - "interfaces": { - "type": "array", - "description": "The list of available network interfaces", - "items": { - "type": "string", - "enum": ["bluetooth", "cellular", "ethernet", "wifi", "wimax", "mixed", "other", "unknown", "none"] - } - }, - "cellular": { - "type": "object", - "description": "Cellular connectivity properties", - "properties": { - "technology": { - "type": "string", - "description": "The type of a radio technology used for cellular connection" - }, - "carrier_name": { - "type": "string", - "description": "The name of the SIM carrier" - } - } - } - } - }, - "_dd": { - "type": "object", - "description": "Internal properties", - "required": [ - "format_version" - ], - "properties": { - "format_version": { - "type": "integer", - "const": 2, - "description": "Version of the RUM event format" - } - } - } - } -} diff --git a/dd-sdk-android/src/main/json/action-schema.json b/dd-sdk-android/src/main/json/action-schema.json deleted file mode 100644 index ca7cabfbc8..0000000000 --- a/dd-sdk-android/src/main/json/action-schema.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "action-schema.json", - "type": "object", - "description": "Schema of all properties of an Action event", - "allOf": [ - { - "$ref": "_common-schema.json" - }, - { - "required": [ - "type", - "action" - ], - "properties": { - "type": { - "type": "string", - "description": "RUM event type", - "const": "action" - }, - "action": { - "type": "object", - "description": "Action properties", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "description": "Type of the action", - "enum": ["custom", "click", "tap", "scroll", "swipe", "application_start", "back"] - }, - "id": { - "type": "string", - "description": "UUID of the action", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - }, - "loading_time": { - "type": "integer", - "description": "Duration in ns to the action is considered loaded", - "minimum": 0 - }, - "target": { - "type": "object", - "description": "Action target properties", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "description": "Target name" - } - } - }, - "error": { - "type": "object", - "description": "Properties of the errors of the action", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of errors that occurred on the action", - "minimum": 0 - } - } - }, - "crash": { - "type": "object", - "description": "Properties of the crashes of the action", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of crashes that occurred on the action", - "minimum": 0 - } - } - }, - "long_task": { - "type": "object", - "description": "Properties of the long tasks of the action", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of long tasks that occurred on the action", - "minimum": 0 - } - } - }, - "resource": { - "type": "object", - "description": "Properties of the resources of the action", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of resources that occurred on the action", - "minimum": 0 - } - } - } - } - } - } - } - ] -} diff --git a/dd-sdk-android/src/main/json/error-schema.json b/dd-sdk-android/src/main/json/error-schema.json deleted file mode 100644 index 532e50bcb2..0000000000 --- a/dd-sdk-android/src/main/json/error-schema.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "error-schema.json", - "type": "object", - "description": "Schema of all properties of an Error event", - "allOf": [ - { - "$ref": "_common-schema.json" - }, - { - "required": [ - "type", - "error" - ], - "properties": { - "type": { - "type": "string", - "description": "RUM event type", - "const": "error" - }, - "error": { - "type": "object", - "description": "Error properties", - "required": [ - "message", - "source" - ], - "properties": { - "message": { - "type": "string", - "description": "Error message" - }, - "source": { - "type": "string", - "description": "Source of the error", - "enum": ["network", "source", "console", "logger", "agent", "webview"] - }, - "stack": { - "type": "string", - "description": "Stacktrace of the error" - }, - "is_crash": { - "type": "boolean", - "description": "Whether this error crashed the host application" - }, - "resource": { - "type": "object", - "description": "Resource properties of the error", - "required": [ - "method", - "status_code", - "url" - ], - "properties": { - "method": { - "type": "string", - "description": "HTTP method of the resource", - "enum": ["POST", "GET", "HEAD", "PUT", "DELETE", "PATCH"] - }, - "status_code": { - "type": "integer", - "description": "HTTP Status code of the resource", - "minimum": 0 - }, - "url": { - "type": "string", - "description": "URL of the resource" - } - } - } - } - }, - "action": { - "type": "object", - "description": "Action properties", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string", - "description": "UUID of the action", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - } - } - } - } - } - ] -} diff --git a/dd-sdk-android/src/main/json/long_task-schema.json b/dd-sdk-android/src/main/json/long_task-schema.json deleted file mode 100644 index 3e22f4e587..0000000000 --- a/dd-sdk-android/src/main/json/long_task-schema.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "long_task-schema.json", - "type": "object", - "description": "Schema of all properties of a Long Task event", - "allOf": [ - { - "$ref": "_common-schema.json" - }, - { - "required": [ - "type", - "long_task" - ], - "properties": { - "type": { - "type": "string", - "description": "RUM event type", - "const": "long_task" - }, - "long_task": { - "type": "object", - "description": "Long Task properties", - "required": [ - "duration" - ], - "properties": { - "duration": { - "type": "integer", - "description": "Duration in ns of the long task", - "minimum": 0 - } - } - }, - "action": { - "type": "object", - "description": "Action properties", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string", - "description": "UUID of the action", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - } - } - } - } - } - ] -} diff --git a/dd-sdk-android/src/main/json/resource-schema.json b/dd-sdk-android/src/main/json/resource-schema.json deleted file mode 100644 index 3303c30e4a..0000000000 --- a/dd-sdk-android/src/main/json/resource-schema.json +++ /dev/null @@ -1,223 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "resource-schema.json", - "type": "object", - "description": "Schema of all properties of a Resource event", - "allOf": [ - { - "$ref": "_common-schema.json" - }, - { - "required": [ - "type", - "resource" - ], - "properties": { - "type": { - "type": "string", - "description": "RUM event type", - "const": "resource" - }, - "resource": { - "type": "object", - "description": "Resource properties", - "required": [ - "type", - "url", - "duration" - ], - "properties": { - "id": { - "type": "string", - "description": "UUID of the resource", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - }, - "type": { - "type": "string", - "description": "Resource type", - "enum": ["document", "xhr", "beacon", "fetch", "css", "js", "image", "font", "media", "other"] - }, - "method": { - "type": "string", - "description": "HTTP method of the resource", - "enum": ["POST", "GET", "HEAD", "PUT", "DELETE", "PATCH"] - }, - "url": { - "type": "string", - "description": "URL of the resource" - }, - "status_code": { - "type": "integer", - "description": "HTTP status code of the resource", - "minimum": 0 - }, - "duration": { - "type": "integer", - "description": "Duration of the resource", - "minimum": 0 - }, - "size": { - "type": "integer", - "description": "Size in octet of the resource response body", - "minimum": 0 - }, - "redirect": { - "type": "object", - "description": "Redirect phase properties", - "required": [ - "duration", - "start" - ], - "properties": { - "duration": { - "type": "integer", - "description": "Duration in ns of the resource redirect phase", - "minimum": 0 - }, - "start": { - "type": "integer", - "description": "Duration in ns between start of the request and start of the redirect phase", - "minimum": 0 - } - } - }, - "dns": { - "type": "object", - "description": "DNS phase properties", - "required": [ - "duration", - "start" - ], - "properties": { - "duration": { - "type": "integer", - "description": "Duration in ns of the resource dns phase", - "minimum": 0 - }, - "start": { - "type": "integer", - "description": "Duration in ns between start of the request and start of the dns phase", - "minimum": 0 - } - } - }, - "connect": { - "type": "object", - "description": "Connect phase properties", - "required": [ - "duration", - "start" - ], - "properties": { - "duration": { - "type": "integer", - "description": "Duration in ns of the resource connect phase", - "minimum": 0 - }, - "start": { - "type": "integer", - "description": "Duration in ns between start of the request and start of the connect phase", - "minimum": 0 - } - } - }, - "ssl": { - "type": "object", - "description": "SSL phase properties", - "required": [ - "duration", - "start" - ], - "properties": { - "duration": { - "type": "integer", - "description": "Duration in ns of the resource ssl phase", - "minimum": 0 - }, - "start": { - "type": "integer", - "description": "Duration in ns between start of the request and start of the ssl phase", - "minimum": 0 - } - } - }, - "first_byte": { - "type": "object", - "description": "First Byte phase properties", - "required": [ - "duration", - "start" - ], - "properties": { - "duration": { - "type": "integer", - "description": "Duration in ns of the resource first byte phase", - "minimum": 0 - }, - "start": { - "type": "integer", - "description": "Duration in ns between start of the request and start of the first byte phase", - "minimum": 0 - } - } - }, - "download": { - "type": "object", - "description": "Download phase properties", - "required": [ - "duration", - "start" - ], - "properties": { - "duration": { - "type": "integer", - "description": "Duration in ns of the resource download phase", - "minimum": 0 - }, - "start": { - "type": "integer", - "description": "Duration in ns between start of the request and start of the download phase", - "minimum": 0 - } - } - }, - "first_party": { - "type": "boolean", - "description": "Whether the resource is calling a first party host" - } - } - }, - "action": { - "type": "object", - "description": "Action properties", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string", - "description": "UUID of the action", - "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" - } - } - }, - "_dd": { - "type": "object", - "description": "Internal properties", - "properties": { - "span_id": { - "type": "string", - "description": "span identifier in decimal format", - "pattern": "^[0-9]+$" - }, - "trace_id": { - "type": "string", - "description": "trace identifier in decimal format", - "pattern": "^[0-9]+$" - } - } - } - } - } - ] -} diff --git a/dd-sdk-android/src/main/json/view-schema.json b/dd-sdk-android/src/main/json/view-schema.json deleted file mode 100644 index b9048f51b5..0000000000 --- a/dd-sdk-android/src/main/json/view-schema.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "view-schema.json", - "type": "object", - "description": "Schema of all properties of a View event", - "allOf": [ - { - "$ref": "_common-schema.json" - }, - { - "required": [ - "type", - "view", - "_dd" - ], - "properties": { - "type": { - "type": "string", - "description": "RUM event type", - "const": "view" - }, - "view": { - "type": "object", - "description": "View properties", - "required": [ - "id", - "url", - "time_spent", - "action", - "error", - "resource" - ], - "properties": { - "loading_time": { - "type": "integer", - "description": "Duration in ns to the view is considered loaded", - "minimum": 0 - }, - "loading_type": { - "type": "string", - "description": "Type of the loading of the view", - "enum": ["initial_load", "route_change", "activity_display", "activity_redisplay", "fragment_display", "fragment_redisplay"] - }, - "time_spent": { - "type": "integer", - "description": "Time spent on the view in ns", - "minimum": 0 - }, - "first_contentful_paint": { - "type": "integer", - "description": "Duration in ns to the first rendering", - "minimum": 0 - }, - "dom_complete": { - "type": "integer", - "description": "Duration in ns to the complete parsing and loading of the document and its sub resources", - "minimum": 0 - }, - "dom_content_loaded": { - "type": "integer", - "description": "Duration in ns to the complete parsing and loading of the document without its sub resources", - "minimum": 0 - }, - "dom_interactive": { - "type": "integer", - "description": "Duration in ns to the end of the parsing of the document", - "minimum": 0 - }, - "load_event": { - "type": "integer", - "description": "Duration in ns to the end of the load event handler execution", - "minimum": 0 - }, - "action": { - "type": "object", - "description": "Properties of the actions of the view", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of actions that occurred on the view", - "minimum": 0 - } - } - }, - "error": { - "type": "object", - "description": "Properties of the errors of the view", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of errors that occurred on the view", - "minimum": 0 - } - } - }, - "crash": { - "type": "object", - "description": "Properties of the crashes of the view", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of crashes that occurred on the view", - "minimum": 0 - } - } - }, - "long_task": { - "type": "object", - "description": "Properties of the long tasks of the view", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of long tasks that occurred on the view", - "minimum": 0 - } - } - }, - "resource": { - "type": "object", - "description": "Properties of the resources of the view", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "description": "Number of resources that occurred on the view", - "minimum": 0 - } - } - } - } - }, - "_dd": { - "type": "object", - "description": "Internal properties", - "required": [ - "document_version" - ], - "properties": { - "document_version": { - "type": "integer", - "description": "Version of the update of the view event", - "minimum": 0 - } - } - } - } - } - ] -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/Datadog.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/Datadog.kt deleted file mode 100644 index 2df54167d1..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/Datadog.kt +++ /dev/null @@ -1,339 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import android.app.Application -import android.content.Context -import android.content.pm.ApplicationInfo -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.lifecycle.ProcessLifecycleCallback -import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.error.internal.CrashReportsFeature -import com.datadog.android.log.EndpointUpdateStrategy -import com.datadog.android.log.internal.LogsFeature -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.tracing.internal.TracesFeature -import java.lang.IllegalArgumentException -import java.util.Locale -import java.util.concurrent.atomic.AtomicBoolean - -/** - * This class initializes the Datadog SDK, and sets up communication with the server. - */ -@Suppress("TooManyFunctions") -object Datadog { - - /** - * The endpoint for our US based servers, used by default by the SDK. - * @see [initialize] - * @deprecated Use the [DatadogEndpoint.LOGS_US] instead - */ - @Suppress("MemberVisibilityCanBePrivate") - @Deprecated( - "Use the DatadogEndpoint.LOGS_US instead", - ReplaceWith( - expression = "DatadogEndpoint.LOGS_US", - imports = ["com.datadog.android.DatadogEndpoint"] - ) - ) - const val DATADOG_US: String = "https://mobile-http-intake.logs.datadoghq.com/" - - /** - * The endpoint for our Europe based servers. - * Use this in your call to [initialize] if you log on - * [app.datadoghq.eu](https://app.datadoghq.eu/) instead of - * [app.datadoghq.com](https://app.datadoghq.com/) - * @deprecated Use the [DatadogEndpoint.LOGS_EU] instead - */ - @Suppress("MemberVisibilityCanBePrivate") - @Deprecated( - "Use the DatadogEndpoint.LOGS_EU instead", - ReplaceWith( - expression = "DatadogEndpoint.LOGS_EU", - imports = ["com.datadog.android.DatadogEndpoint"] - ) - ) - const val DATADOG_EU: String = "https://mobile-http-intake.logs.datadoghq.eu/" - - internal val initialized = AtomicBoolean(false) - internal val startupTimeNs = System.nanoTime() - - internal var libraryVerbosity = Int.MAX_VALUE - private set - internal var isDebug = false - - /** - * Initializes the Datadog SDK. - * @param context your application context - * @param config the configuration for the SDK library - * @see [DatadogConfig] - * @throws IllegalArgumentException if the env name is using illegal characters and your - * application is in debug mode otherwise returns false and stops initializing the SDK - * @deprecated Use the [Datadog.initialize] instead which requires - * a privacy [TrackingConsent] parameter. - */ - @Deprecated( - "This method is deprecated and uses the [TrackingConsent.GRANTED] " + - "flag as a default privacy consent.This means that the SDK will start recording " + - "and sending data immediately after initialisation without waiting " + - "for the user's consent to be tracked.", - ReplaceWith( - expression = "Datadog.initialize(context, TrackingConsent.PENDING, config)", - imports = ["com.datadog.android.privacy.TrackingConsent"] - ) - ) - @Suppress("LongMethod") - @JvmStatic - fun initialize( - context: Context, - config: DatadogConfig - ) { - initialize(context, TrackingConsent.GRANTED, config) - } - - /** - * Initializes the Datadog SDK. - * @param context your application context - * @param trackingConsent as the initial state of the tracking consent flag. - * @param config the configuration for the SDK library - * @see [DatadogConfig] - * @see [TrackingConsent] - * @throws IllegalArgumentException if the env name is using illegal characters and your - * application is in debug mode otherwise returns false and stops initializing the SDK - */ - @Suppress("LongMethod") - @JvmStatic - fun initialize( - context: Context, - trackingConsent: TrackingConsent, - config: DatadogConfig - ) { - if (initialized.get()) { - devLogger.w(MESSAGE_ALREADY_INITIALIZED) - return - } - - val appContext = context.applicationContext - // the logic in this function depends on this value so always resolve isDebug first - isDebug = resolveIsDebug(context) - - if (!validateCoreConfig(config.coreConfig)) { - return - } - - // always initialize Core Features first - CoreFeature.initialize(appContext, trackingConsent, config.coreConfig) - - config.logsConfig?.let { featureConfig -> - LogsFeature.initialize( - appContext = appContext, - config = featureConfig, - okHttpClient = CoreFeature.okHttpClient, - networkInfoProvider = CoreFeature.networkInfoProvider, - systemInfoProvider = CoreFeature.systemInfoProvider, - dataUploadThreadPoolExecutor = CoreFeature.dataUploadScheduledExecutor, - dataPersistenceExecutor = CoreFeature.dataPersistenceExecutorService, - trackingConsentProvider = CoreFeature.trackingConsentProvider - ) - } - - config.tracesConfig?.let { featureConfig -> - TracesFeature.initialize( - appContext = appContext, - config = featureConfig, - okHttpClient = CoreFeature.okHttpClient, - networkInfoProvider = CoreFeature.networkInfoProvider, - timeProvider = CoreFeature.timeProvider, - userInfoProvider = CoreFeature.userInfoProvider, - systemInfoProvider = CoreFeature.systemInfoProvider, - dataUploadThreadPoolExecutor = CoreFeature.dataUploadScheduledExecutor, - dataPersistenceExecutor = CoreFeature.dataPersistenceExecutorService, - trackingConsentProvider = CoreFeature.trackingConsentProvider - ) - } - - config.rumConfig?.let { featureConfig -> - RumFeature.initialize( - appContext = appContext, - config = featureConfig, - okHttpClient = CoreFeature.okHttpClient, - networkInfoProvider = CoreFeature.networkInfoProvider, - systemInfoProvider = CoreFeature.systemInfoProvider, - dataUploadThreadPoolExecutor = CoreFeature.dataUploadScheduledExecutor, - dataPersistenceExecutor = CoreFeature.dataPersistenceExecutorService, - userInfoProvider = CoreFeature.userInfoProvider, - trackingConsentProvider = CoreFeature.trackingConsentProvider - ) - } - - config.crashReportConfig?.let { featureConfig -> - CrashReportsFeature.initialize( - appContext = appContext, - config = featureConfig, - okHttpClient = CoreFeature.okHttpClient, - networkInfoProvider = CoreFeature.networkInfoProvider, - userInfoProvider = CoreFeature.userInfoProvider, - systemInfoProvider = CoreFeature.systemInfoProvider, - dataUploadThreadPoolExecutor = CoreFeature.dataUploadScheduledExecutor, - dataPersistenceExecutor = CoreFeature.dataPersistenceExecutorService, - trackingConsentProvider = CoreFeature.trackingConsentProvider - ) - } - - setupLifecycleMonitorCallback(appContext) - - initialized.set(true) - - // Issue #154 (“Thread starting during runtime shutdown”) - // Make sure we stop Datadog when the Runtime shuts down - Runtime.getRuntime() - .addShutdownHook( - Thread(Runnable { stop() }, SHUTDOWN_THREAD) - ) - } - - /** - * Changes the endpoint to which logging data is sent. - * @param endpointUrl the endpoint url to target, or null to use the default. - * Possible values are [DATADOG_US_LOGS], [DATADOG_EU_LOGS] or a custom endpoint. - * @param strategy the strategy defining how to handle logs created previously. - * Because logs are sent asynchronously, some logs intended for the previous endpoint - * might still be yet to sent. - */ - @Suppress("DeprecatedCallableAddReplaceWith") - @JvmStatic - @Deprecated("This was only meant as an internal feature and is not needed anymore.") - fun setEndpointUrl(endpointUrl: String, strategy: EndpointUpdateStrategy) { - devLogger.w(String.format(Locale.US, MESSAGE_DEPRECATED, "setEndpointUrl()")) - } - - /** - * Checks if the Datadog SDK was already initialized. - * @return true if the SDK was initialized, false otherwise - */ - fun isInitialized(): Boolean { - return initialized.get() - } - - /** - * Clears all data that has not already been sent to Datadog servers. - */ - fun clearAllData() { - LogsFeature.clearAllData() - CrashReportsFeature.clearAllData() - RumFeature.clearAllData() - TracesFeature.clearAllData() - } - - // Stop all Datadog work (for test purposes). - @Suppress("unused") - private fun stop() { - if (initialized.get()) { - LogsFeature.stop() - TracesFeature.stop() - RumFeature.stop() - CrashReportsFeature.stop() - CoreFeature.stop() - isDebug = false - initialized.set(false) - } - } - - /** - * Sets the verbosity of the Datadog library. - * - * Messages with a priority level equal or above the given level will be sent to Android's - * Logcat. - * - * @param level one of the Android [Log] constants ([Log.VERBOSE], [Log.DEBUG], [Log.INFO], - * [Log.WARN], [Log.ERROR], [Log.ASSERT]). - */ - @JvmStatic - fun setVerbosity(level: Int) { - libraryVerbosity = level - } - - /** - * Sets the tracking consent regarding the data collection for the Datadog library. - * - * @param consent which can take one of the values - * ([TrackingConsent.PENDING], [TrackingConsent.GRANTED], [TrackingConsent.NOT_GRANTED]) - */ - fun setTrackingConsent(consent: TrackingConsent) { - CoreFeature.trackingConsentProvider.setConsent(consent) - } - - /** - * Sets the user information. - * - * @param id (nullable) a unique user identifier (relevant to your business domain) - * @param name (nullable) the user name or alias - * @param email (nullable) the user email - * @param extraInfo additional information. An extra information can be - * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. - */ - @JvmStatic - @JvmOverloads - fun setUserInfo( - id: String? = null, - name: String? = null, - email: String? = null, - extraInfo: Map = emptyMap() - ) { - CoreFeature.userInfoProvider.setUserInfo(UserInfo(id, name, email, extraInfo)) - } - - // region Internal Initialization - - @Suppress("ThrowingInternalException") - private fun validateCoreConfig(config: DatadogConfig.CoreConfig): Boolean { - if (!config.envName.matches(Regex(ENV_NAME_VALIDATION_REG_EX))) { - if (isDebug) { - throw IllegalArgumentException(MESSAGE_ENV_NAME_NOT_VALID) - } else { - devLogger.e(MESSAGE_ENV_NAME_NOT_VALID) - return false - } - } - return true - } - - private fun setupLifecycleMonitorCallback(appContext: Context) { - if (appContext is Application) { - val callback = ProcessLifecycleCallback(CoreFeature.networkInfoProvider, appContext) - appContext.registerActivityLifecycleCallbacks(ProcessLifecycleMonitor(callback)) - } - } - - private fun resolveIsDebug(context: Context): Boolean { - return (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 - } - - internal const val MESSAGE_ALREADY_INITIALIZED = - "The Datadog library has already been initialized." - internal const val MESSAGE_NOT_INITIALIZED = "Datadog has not been initialized.\n" + - "Please add the following code in your application's onCreate() method:\n" + - "val config = DatadogConfig.Builder(\"\", \"\", " + - "\"\").build()\n" + - "Datadog.initialize(context, config);" - - internal const val MESSAGE_DEPRECATED = "%s has been deprecated. " + - "If you need it, submit an issue at https://github.com/DataDog/dd-sdk-android/issues/" - - internal const val SHUTDOWN_THREAD = "datadog_shutdown" - internal const val ENV_NAME_VALIDATION_REG_EX = "[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]" - internal const val MESSAGE_ENV_NAME_NOT_VALID = - "The environment name should contain maximum 196 of the following allowed characters " + - "[a-zA-Z0-9_:./-] and should never finish with a semicolon." + - "In this case the Datadog SDK will not be initialised." - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogConfig.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogConfig.kt deleted file mode 100644 index 66f1f550ec..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogConfig.kt +++ /dev/null @@ -1,421 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import android.os.Build -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.Feature -import com.datadog.android.rum.internal.instrumentation.GesturesTrackingStrategy -import com.datadog.android.rum.internal.instrumentation.GesturesTrackingStrategyApi29 -import com.datadog.android.rum.internal.instrumentation.gestures.DatadogGesturesTracker -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.rum.internal.tracking.JetpackViewAttributesProvider -import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy -import com.datadog.android.rum.tracking.ViewAttributesProvider -import com.datadog.android.rum.tracking.ViewTrackingStrategy -import java.util.UUID - -/** - * An object describing the configuration of the Datadog SDK. - * - * This is necessary to initialize the SDK with the [Datadog.initialize] method. - */ -class DatadogConfig -private constructor( - internal val logsConfig: FeatureConfig?, - internal val tracesConfig: FeatureConfig?, - internal val crashReportConfig: FeatureConfig?, - internal val rumConfig: RumConfig?, - internal var coreConfig: CoreConfig -) { - - internal data class CoreConfig( - var needsClearTextHttp: Boolean = false, - val envName: String = "", - val serviceName: String? = null, - val hosts: List = emptyList() - ) - - internal data class FeatureConfig( - val clientToken: String, - val applicationId: UUID, - val endpointUrl: String, - val envName: String, - val plugins: List = emptyList() - ) - - internal data class RumConfig( - val clientToken: String, - val applicationId: UUID, - val endpointUrl: String, - val envName: String, - val samplingRate: Float = 100.0f, - val gesturesTracker: GesturesTracker? = null, - val userActionTrackingStrategy: UserActionTrackingStrategy? = null, - val viewTrackingStrategy: ViewTrackingStrategy? = null, - val plugins: List = emptyList() - ) - - // region Builder - - /** - * A Builder class for a [DatadogConfig]. - * @param clientToken your API key of type Client Token - * @param envName the environment name special attribute that will be sent with each event. - * This can be used to filter your events on different environments - * (e.g. "staging" vs. "production"). - * @param applicationId your applicationId for RUM events - - */ - @Suppress("TooManyFunctions") - class Builder(clientToken: String, envName: String, applicationId: UUID) { - - /** - * A Builder class for a [DatadogConfig]. - * @param clientToken your API key of type Client Token - * @param envName the environment name special attribute that will be sent with each event. - * This can be used to filter your events on different environments - * (e.g. "staging" vs. "production"). - */ - constructor(clientToken: String, envName: String) : - this(clientToken, envName, UUID(0, 0)) - - /** - * A Builder class for a [DatadogConfig]. - * @param clientToken your API key of type Client Token - * @param envName the environment name special attribute that will be sent with each event. - * This can be used to filter your events on different environments - * (e.g. "staging" vs. "production"). - * @param applicationId your applicationId for RUM events - */ - constructor(clientToken: String, envName: String, applicationId: String) : - this(clientToken, envName, UUID.fromString(applicationId)) - - private var logsConfig: FeatureConfig = FeatureConfig( - clientToken, - applicationId, - DatadogEndpoint.LOGS_US, - envName - ) - private var tracesConfig: FeatureConfig = FeatureConfig( - clientToken, - applicationId, - DatadogEndpoint.TRACES_US, - envName - ) - private var crashReportConfig: FeatureConfig = FeatureConfig( - clientToken, - applicationId, - DatadogEndpoint.LOGS_US, - envName - ) - private var rumConfig: RumConfig = RumConfig( - clientToken, - applicationId, - DatadogEndpoint.RUM_US, - envName - ) - - private var coreConfig = CoreConfig(envName = envName) - - private var logsEnabled: Boolean = true - private var tracesEnabled: Boolean = true - private var crashReportsEnabled: Boolean = true - private var rumEnabled: Boolean = applicationId != UUID(0, 0) - - /** - * Builds a [DatadogConfig] based on the current state of this Builder. - */ - fun build(): DatadogConfig { - - return DatadogConfig( - logsConfig = if (logsEnabled) logsConfig else null, - tracesConfig = if (tracesEnabled) tracesConfig else null, - crashReportConfig = if (crashReportsEnabled) crashReportConfig else null, - rumConfig = if (rumEnabled) rumConfig else null, - coreConfig = coreConfig - ) - } - - /** - * Enables or disables the logs feature. - * This feature is enabled by default, disabling it will prevent any logs to be sent to - * Datadog servers. - * @param enabled true by default - */ - fun setLogsEnabled(enabled: Boolean): Builder { - logsEnabled = enabled - return this - } - - /** - * Enables or disables the tracing feature. - * This feature is enabled by default, disabling it will prevent any spans and traces to - * be sent to Datadog servers. - * @param enabled true by default - */ - fun setTracesEnabled(enabled: Boolean): Builder { - tracesEnabled = enabled - return this - } - - /** - * Enables or disables the crash report feature. - * This feature is enabled by default, disabling it will prevent any crash report to be - * sent to Datadog servers. - * @param enabled true by default - */ - fun setCrashReportsEnabled(enabled: Boolean): Builder { - crashReportsEnabled = enabled - return this - } - - /** - * Enables or disables the Real User Monitoring feature. - * This feature is enabled by default, disabling it will prevent any RUM data to be - * sent to Datadog servers. - * @param enabled true by default - */ - fun setRumEnabled(enabled: Boolean): Builder { - if (enabled && rumConfig.applicationId == UUID(0, 0)) { - devLogger.w(RUM_NOT_INITIALISED_WARNING_MESSAGE) - return this - } - rumEnabled = enabled - return this - } - - /** - * Sets the service name that will appear in your logs, traces and crash reports. - * @param serviceName the service name (default = "android") - */ - fun setServiceName(serviceName: String): Builder { - coreConfig = coreConfig.copy(serviceName = serviceName) - return this - } - - /** - * Sets the environment name that will appear in your logs, traces and crash reports. - * This can be used to filter logs or traces and distinguish between your production - * and staging environment. - * @param envName the environment name (default = "") - */ - @Deprecated("This property is now mandatory for initializing the SDK") - fun setEnvironmentName(envName: String): Builder { - logsConfig = logsConfig.copy(envName = envName) - tracesConfig = tracesConfig.copy(envName = envName) - crashReportConfig = crashReportConfig.copy(envName = envName) - rumConfig = rumConfig.copy(envName = envName) - coreConfig = coreConfig.copy(envName = envName) - return this - } - - /** - * Sets the list of first party hosts. - * Requests made to a URL with any one of these hosts (or any subdomain) will: - * - be considered a first party resource and categorised as such in your RUM dashboard; - * - be wrapped in a Span and have trace id injected to get a full flame-graph in APM. - * @param hosts a list of all the hosts that you own. - * See [DatadogInterceptor] - */ - fun setFirstPartyHosts(hosts: List): Builder { - coreConfig = coreConfig.copy(hosts = hosts) - return this - } - - /** - * Let the SDK target Datadog's Europe server. - * - * Call this if you log on [app.datadoghq.eu](https://app.datadoghq.eu/). - */ - fun useEUEndpoints(): Builder { - logsConfig = logsConfig.copy(endpointUrl = DatadogEndpoint.LOGS_EU) - tracesConfig = tracesConfig.copy(endpointUrl = DatadogEndpoint.TRACES_EU) - crashReportConfig = crashReportConfig.copy(endpointUrl = DatadogEndpoint.LOGS_EU) - rumConfig = rumConfig.copy(endpointUrl = DatadogEndpoint.RUM_EU) - coreConfig = coreConfig.copy(needsClearTextHttp = false) - return this - } - - /** - * Let the SDK target Datadog's US server. - * - * Call this if you log on [app.datadoghq.com](https://app.datadoghq.com/). - */ - fun useUSEndpoints(): Builder { - logsConfig = logsConfig.copy(endpointUrl = DatadogEndpoint.LOGS_US) - tracesConfig = tracesConfig.copy(endpointUrl = DatadogEndpoint.TRACES_US) - crashReportConfig = crashReportConfig.copy(endpointUrl = DatadogEndpoint.LOGS_US) - rumConfig = rumConfig.copy(endpointUrl = DatadogEndpoint.RUM_US) - coreConfig = coreConfig.copy(needsClearTextHttp = false) - return this - } - - /** - * Let the SDK target Datadog's Gov server. - * - * Call this if you log on [app.ddog-gov.com/](https://app.ddog-gov.com/). - */ - fun useGovEndpoints(): Builder { - logsConfig = logsConfig.copy(endpointUrl = DatadogEndpoint.LOGS_GOV) - tracesConfig = tracesConfig.copy(endpointUrl = DatadogEndpoint.TRACES_GOV) - crashReportConfig = crashReportConfig.copy(endpointUrl = DatadogEndpoint.LOGS_GOV) - rumConfig = rumConfig.copy(endpointUrl = DatadogEndpoint.RUM_GOV) - coreConfig = coreConfig.copy(needsClearTextHttp = false) - return this - } - - /** - * Let the SDK target a custom server for the logs feature. - */ - fun useCustomLogsEndpoint(endpoint: String): Builder { - logsConfig = logsConfig.copy(endpointUrl = endpoint) - checkCustomEndpoint(endpoint) - return this - } - - /** - * Let the SDK target a custom server for the tracing feature. - */ - fun useCustomTracesEndpoint(endpoint: String): Builder { - tracesConfig = tracesConfig.copy(endpointUrl = endpoint) - checkCustomEndpoint(endpoint) - return this - } - - /** - * Let the SDK target a custom server for the crash reports feature. - */ - fun useCustomCrashReportsEndpoint(endpoint: String): Builder { - crashReportConfig = crashReportConfig.copy(endpointUrl = endpoint) - checkCustomEndpoint(endpoint) - return this - } - - /** - * Let the SDK target a custom server for the RUM feature. - */ - fun useCustomRumEndpoint(endpoint: String): Builder { - rumConfig = rumConfig.copy(endpointUrl = endpoint) - checkCustomEndpoint(endpoint) - return this - } - - /** - * Enable the user interaction automatic tracker. By enabling this feature the SDK will intercept - * UI interaction events (e.g.: taps, scrolls, swipes) and automatically send those as RUM UserActions for you. - * @param touchTargetExtraAttributesProviders an array with your own implementation of the - * target attributes provider. - * @see [ViewAttributesProvider] - */ - @JvmOverloads - fun trackInteractions( - touchTargetExtraAttributesProviders: Array = emptyArray() - ): Builder { - val gesturesTracker = gestureTracker(touchTargetExtraAttributesProviders) - rumConfig = rumConfig.copy( - gesturesTracker = gesturesTracker, - userActionTrackingStrategy = provideUserTrackingStrategy( - gesturesTracker - ) - ) - return this - } - - /** - * Sets the automatic view tracking strategy used by the SDK. - * By default no view will be tracked. - * @param strategy as the [ViewTrackingStrategy] - * Note: By default, the RUM Monitor will let you handle View events manually. - * This means that you should call [RumMonitor.startView] and [RumMonitor.stopView] - * yourself. A view should be started when it becomes visible and interactive - * (equivalent to `onResume`) and be stopped when it's paused (equivalent to `onPause`). - * @see [com.datadog.android.rum.tracking.ActivityViewTrackingStrategy] - * @see [com.datadog.android.rum.tracking.FragmentViewTrackingStrategy] - * @see [com.datadog.android.rum.tracking.MixedViewTrackingStrategy] - * @see [com.datadog.android.rum.tracking.NavigationViewTrackingStrategy] - - */ - fun useViewTrackingStrategy(strategy: ViewTrackingStrategy): Builder { - rumConfig = rumConfig.copy(viewTrackingStrategy = strategy) - return this - } - - /** - * Adds a plugin to a specific feature. This plugin will only be registered if the feature - * was enabled. - * @param plugin a [DatadogPlugin] - * @param feature the feature for which this plugin should be registered - * @see [Feature.LOG] - * @see [Feature.CRASH] - * @see [Feature.TRACE] - * @see [Feature.RUM] - */ - fun addPlugin(plugin: DatadogPlugin, feature: Feature): Builder { - when (feature) { - Feature.RUM -> rumConfig = rumConfig.copy(plugins = rumConfig.plugins + plugin) - Feature.TRACE -> - tracesConfig = - tracesConfig.copy(plugins = tracesConfig.plugins + plugin) - Feature.LOG -> logsConfig = logsConfig.copy(plugins = logsConfig.plugins + plugin) - Feature.CRASH -> - crashReportConfig = - crashReportConfig.copy(plugins = crashReportConfig.plugins + plugin) - } - - return this - } - - /** - * Sets the sampling rate for RUM Sessions. - * - * @param samplingRate the sampling rate must be a value between 0 and 100. A value of 0 - * means no RUM event will be sent, 100 means all sessions will be kept. - * - */ - fun sampleRumSessions(samplingRate: Float): Builder { - rumConfig = rumConfig.copy(samplingRate = samplingRate) - return this - } - - private fun checkCustomEndpoint(endpoint: String) { - if (endpoint.startsWith("http://")) { - coreConfig = coreConfig.copy(needsClearTextHttp = true) - } - } - - private fun provideUserTrackingStrategy( - gesturesTracker: GesturesTracker - ): - UserActionTrackingStrategy { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - GesturesTrackingStrategyApi29(gesturesTracker) - } else { - GesturesTrackingStrategy(gesturesTracker) - } - } - - private fun gestureTracker(customProviders: Array): - DatadogGesturesTracker { - val defaultProviders = arrayOf(JetpackViewAttributesProvider()) - val providers = customProviders + defaultProviders - return DatadogGesturesTracker(providers) - } - - companion object { - internal const val RUM_NOT_INITIALISED_WARNING_MESSAGE = - "You're trying to enable RUM but no Application Id was provided. " + - "Please use the following line to create your DatadogConfig:\n" + - "val config = " + - "DatadogConfig.Builder" + - "(\"\", \"\", \"\").build()" - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogEndpoint.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogEndpoint.kt deleted file mode 100644 index fd8440be6c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogEndpoint.kt +++ /dev/null @@ -1,101 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import com.datadog.android.Datadog.initialize - -/** - * This object contains constant values for all the Datadog Endpoint urls used in the SDK. - */ -object DatadogEndpoint { - - /** - * The endpoint for Logs (US based servers), used by default by the SDK. - * @see [DatadogConfig] - */ - const val LOGS_US: String = "https://mobile-http-intake.logs.datadoghq.com/" - - /** - * The endpoint for Logs (Europe based servers). - * Use this in your [DatadogConfig] if you log on - * [app.datadoghq.eu](https://app.datadoghq.eu/) instead of - * [app.datadoghq.com](https://app.datadoghq.com/) - */ - const val LOGS_EU: String = "https://mobile-http-intake.logs.datadoghq.eu/" - - /** - * The endpoint for Logs (GovCloud compatible servers). - * Use this in your [DatadogConfig] if you log on - * [app.ddog-gov.com/](https://app.ddog-gov.com/) instead of - * [app.datadoghq.com](https://app.datadoghq.com/) - */ - const val LOGS_GOV: String = "https://mobile-http-intake.logs.ddog-gov.com/" - - /** - * The endpoint for Traces (US based servers), used by default by the SDK. - * @see [initialize] - */ - const val TRACES_US: String = "https://public-trace-http-intake.logs.datadoghq.com/" - - /** - * The endpoint for Traces (Europe based servers). - * Use this in your [DatadogConfig] if you log on - * [app.datadoghq.eu](https://app.datadoghq.eu/) instead of - * [app.datadoghq.com](https://app.datadoghq.com/) - */ - const val TRACES_EU: String = "https://public-trace-http-intake.logs.datadoghq.eu/" - - /** - * The endpoint for Traces (GovCloud compatible servers). - * Use this in your [DatadogConfig] if you log on - * [app.ddog-gov.com/](https://app.ddog-gov.com/) instead of - * [app.datadoghq.com](https://app.datadoghq.com/) - */ - const val TRACES_GOV: String = "https://public-trace-http-intake.logs.ddog-gov.com/" - - /** - * The endpoint for Real User Monitoring (US based servers), used by default by the SDK. - * @see [DatadogConfig] - */ - const val RUM_US: String = "https://rum-http-intake.logs.datadoghq.com/" - - /** - * The endpoint for Real User Monitoring (Europe based servers). - * Use this in your [DatadogConfig] if you log on - * [app.datadoghq.eu](https://app.datadoghq.eu/) instead of - * [app.datadoghq.com](https://app.datadoghq.com/) - */ - const val RUM_EU: String = "https://rum-http-intake.logs.datadoghq.eu/" - - /** - * The endpoint for Real User Monitoring (GovCloud compatible servers). - * Use this in your [DatadogConfig] if you log on - * [app.ddog-gov.com/](https://app.ddog-gov.com/) instead of - * [app.datadoghq.com](https://app.datadoghq.com/) - */ - const val RUM_GOV: String = "https://rum-http-intake.logs.ddog-gov.com/" - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - const val NTP_0: String = "0.datadog.pool.ntp.org" - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - const val NTP_1: String = "1.datadog.pool.ntp.org" - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - const val NTP_2: String = "2.datadog.pool.ntp.org" - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - const val NTP_3: String = "3.datadog.pool.ntp.org" -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogEventListener.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogEventListener.kt deleted file mode 100644 index 1ace07f87f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogEventListener.kt +++ /dev/null @@ -1,223 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import com.datadog.android.DatadogEventListener.Factory -import com.datadog.android.core.internal.net.identifyRequest -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import java.io.IOException -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.Proxy -import okhttp3.Call -import okhttp3.EventListener -import okhttp3.Handshake -import okhttp3.OkHttpClient -import okhttp3.Protocol -import okhttp3.Response - -/** - * Datadog's RUM implementation of OkHttp [EventListener]. - * - * This will track requests timing information (TTFB, DNS resolution, …) and append it - * to RUM Resource events. - * - * To use: - * ``` - * OkHttpClient client = new OkHttpClient.Builder() - * .addInterceptor(new DatadogInterceptor()) - * .eventListenerFactory(new DatadogEventListener.Factory()) - * .build(); - * ``` - * - * @see [Factory] - */ -class DatadogEventListener -internal constructor(val key: String) : EventListener() { - - private var callStart = 0L - - private var dnsStart = 0L - private var dnsEnd = 0L - - private var connStart = 0L - private var connEnd = 0L - - private var sslStart = 0L - private var sslEnd = 0L - - private var headersStart = 0L - private var headersEnd = 0L - - private var bodyStart = 0L - private var bodyEnd = 0L - - // region EventListener - - /** @inheritdoc */ - override fun callStart(call: Call) { - super.callStart(call) - (GlobalRum.get() as? AdvancedRumMonitor)?.waitForResourceTiming(key) - callStart = System.nanoTime() - } - - /** @inheritdoc */ - override fun dnsStart(call: Call, domainName: String) { - super.dnsStart(call, domainName) - dnsStart = System.nanoTime() - } - - /** @inheritdoc */ - override fun dnsEnd(call: Call, domainName: String, inetAddressList: MutableList) { - super.dnsEnd(call, domainName, inetAddressList) - dnsEnd = System.nanoTime() - } - - /** @inheritdoc */ - override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { - super.connectStart(call, inetSocketAddress, proxy) - connStart = System.nanoTime() - } - - /** @inheritdoc */ - override fun connectEnd( - call: Call, - inetSocketAddress: InetSocketAddress, - proxy: Proxy, - protocol: Protocol? - ) { - super.connectEnd(call, inetSocketAddress, proxy, protocol) - connEnd = System.nanoTime() - } - - /** @inheritdoc */ - override fun secureConnectStart(call: Call) { - super.secureConnectStart(call) - sslStart = System.nanoTime() - } - - /** @inheritdoc */ - override fun secureConnectEnd(call: Call, handshake: Handshake?) { - super.secureConnectEnd(call, handshake) - sslEnd = System.nanoTime() - } - - /** @inheritdoc */ - override fun responseHeadersStart(call: Call) { - super.responseHeadersStart(call) - headersStart = System.nanoTime() - } - - /** @inheritdoc */ - override fun responseHeadersEnd(call: Call, response: Response) { - super.responseHeadersEnd(call, response) - headersEnd = System.nanoTime() - if (response.code() >= 400) { - sendTiming() - } - } - - /** @inheritdoc */ - override fun responseBodyStart(call: Call) { - super.responseBodyStart(call) - bodyStart = System.nanoTime() - } - - /** @inheritdoc */ - override fun responseBodyEnd(call: Call, byteCount: Long) { - super.responseBodyEnd(call, byteCount) - bodyEnd = System.nanoTime() - } - - /** @inheritdoc */ - override fun callEnd(call: Call) { - super.callEnd(call) - sendTiming() - } - - /** @inheritdoc */ - override fun callFailed(call: Call, ioe: IOException) { - super.callFailed(call, ioe) - sendTiming() - } - - // endregion - - // region Internal - - private fun sendTiming() { - - val timing = buildTiming() - (GlobalRum.get() as? AdvancedRumMonitor)?.addResourceTiming(key, timing) - } - - private fun buildTiming(): ResourceTiming { - val (dnsS, dnsD) = if (dnsStart == 0L) { - 0L to 0L - } else { - (dnsStart - callStart) to (dnsEnd - dnsStart) - } - val (conS, conD) = if (connStart == 0L) { - 0L to 0L - } else { - (connStart - callStart) to (connEnd - connStart) - } - val (sslS, sslD) = if (sslStart == 0L) { - 0L to 0L - } else (sslStart - callStart) to (sslEnd - sslStart) - val (fbS, fbD) = if (headersStart == 0L) { - 0L to 0L - } else { - (headersStart - callStart) to (headersEnd - headersStart) - } - val (dlS, dlD) = if (bodyStart == 0L) { - 0L to 0L - } else { - (bodyStart - callStart) to (bodyEnd - bodyStart) - } - - return ResourceTiming( - dnsStart = dnsS, - dnsDuration = dnsD, - connectStart = conS, - connectDuration = conD, - sslStart = sslS, - sslDuration = sslD, - firstByteStart = fbS, - firstByteDuration = fbD, - downloadStart = dlS, - downloadDuration = dlD - ) - } - - // endregion - - /** - * Datadog's RUM implementation of OkHttp [EventListener.Factory]. - * Adding this Factory to your [OkHttpClient] will allow Datadog to monitor - * timing information for your requests (DNS resolution, TTFB, …). - * - * The timing information will be appended to the relevant RUM Resource events. - * - * To use: - * ``` - * OkHttpClient client = new OkHttpClient.Builder() - * .addInterceptor(new DatadogInterceptor()) - * .eventListenerFactory(new DatadogEventListener.Factory()) - * .build(); - * ``` - */ - class Factory : EventListener.Factory { - /** @inheritdoc */ - override fun create(call: Call): EventListener { - val key = identifyRequest(call.request()) - return DatadogEventListener(key) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogInterceptor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogInterceptor.kt deleted file mode 100644 index d16f0ffe54..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/DatadogInterceptor.kt +++ /dev/null @@ -1,206 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.net.identifyRequest -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumInterceptor -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.tracking.ViewTrackingStrategy -import com.datadog.android.tracing.AndroidTracer -import com.datadog.android.tracing.NoOpTracedRequestListener -import com.datadog.android.tracing.TracedRequestListener -import com.datadog.android.tracing.TracingInterceptor -import io.opentracing.Span -import io.opentracing.Tracer -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response - -/** - * Provides automatic integration for [OkHttpClient] by way of the [Interceptor] system. - * - * This interceptor will combine the effects of the [TracingInterceptor] and the - * [RumInterceptor]. - * - * From [RumInterceptor]: this interceptor will log the request as a RUM Resource, and fill the - * request information (url, method, status code, optional error). Note that RUM Resources are only - * tracked when a view is active. You can use one of the existing [ViewTrackingStrategy] when - * configuring the SDK (see [DatadogConfig.Builder.useViewTrackingStrategy]) or start a view - * manually (see [RumMonitor.startView]). - * - * From [TracingInterceptor]: This interceptor will create a [Span] around the request and fill the - * request information (url, method, status code, optional error). It will also propagate the span - * and trace information in the request header to link it with backend spans. - * - * Note: If you want to get more insights on the network requests (such as redirections), you can also add - * this interceptor as a Network level interceptor. - * - * To use: - * ``` - * val tracedHosts = listOf("example.com", "example.eu") - * OkHttpClient client = new OkHttpClient.Builder() - * .addInterceptor(new DatadogInterceptor(tracedHosts)) - * // Optionally to get information about redirections and retries - * // .addNetworkInterceptor(new TracingInterceptor(tracedHosts)) - * .build(); - * ``` - * - * @param tracedHosts a list of all the hosts that you want to be automatically tracked - * by our APM [TracingInterceptor]. If no host provided the interceptor won't trace - * any OkHttpRequest, nor propagate tracing information to the backend. - * Please note that the host constraint will only be applied on the [TracingInterceptor] and we will - * continue to dispatch RUM Resource events for each request without applying any host filtering. - * @param tracedRequestListener which listens on the intercepted [okhttp3.Request] and offers - * the possibility to modify the created [io.opentracing.Span]. - * - */ -open class DatadogInterceptor -internal constructor( - tracedHosts: List, - tracedRequestListener: TracedRequestListener, - firstPartyHostDetector: FirstPartyHostDetector, - localTracerFactory: () -> Tracer -) : TracingInterceptor( - tracedHosts, - tracedRequestListener, - firstPartyHostDetector, - localTracerFactory -) { - - /** - * Creates a [TracingInterceptor] to automatically create a trace around OkHttp [Request]s, and - * track RUM Resources. - * - * @param tracedHosts a list of all the hosts that you want to be automatically tracked - * by our APM [TracingInterceptor]. If no host provided the interceptor won't trace - * any OkHttp [Request], nor propagate tracing information to the backend. - * Please note that the host constraint will only be applied on the [TracingInterceptor] and we - * will continue to dispatch RUM Resource events for each request without applying any host - * filtering. - * @param tracedRequestListener which listens on the intercepted [okhttp3.Request] and offers - * the possibility to modify the created [io.opentracing.Span]. - */ - @JvmOverloads - constructor( - tracedHosts: List, - tracedRequestListener: TracedRequestListener = NoOpTracedRequestListener() - ) : this( - tracedHosts, - tracedRequestListener, - CoreFeature.firstPartyHostDetector, - { AndroidTracer.Builder().build() } - ) - - // region Interceptor - - /** @inheritdoc */ - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val url = request.url().toString() - val method = request.method() - val requestId = identifyRequest(request) - - GlobalRum.get().startResource(requestId, method, url) - - return super.intercept(chain) - } - - // endregion - - // region TracingInterceptor - - /** @inheritdoc */ - override fun onRequestIntercepted( - request: Request, - span: Span?, - response: Response?, - throwable: Throwable? - ) { - super.onRequestIntercepted(request, span, response, throwable) - - if (throwable != null) { - handleThrowable(request, throwable) - } else { - handleResponse(request, response, span) - } - } - - // endregion - - // region Internal - - private fun handleResponse( - request: Request, - response: Response?, - span: Span? - ) { - val requestId = identifyRequest(request) - val statusCode = response?.code() - val method = request.method() - val mimeType = response?.header(HEADER_CT) - val kind = when { - method in xhrMethods -> RumResourceKind.XHR - mimeType == null -> RumResourceKind.UNKNOWN - else -> RumResourceKind.fromMimeType(mimeType) - } - val attributes = if (span == null) { - emptyMap() - } else { - mapOf( - RumAttributes.TRACE_ID to span.context().toTraceId(), - RumAttributes.SPAN_ID to span.context().toSpanId() - ) - } - GlobalRum.get().stopResource( - requestId, - statusCode, - getBodyLength(response), - kind, - attributes - ) - } - - private fun handleThrowable( - request: Request, - throwable: Throwable - ) { - val requestId = identifyRequest(request) - val method = request.method() - val url = request.url().toString() - GlobalRum.get().stopResourceWithError( - requestId, - null, - ERROR_MSG_FORMAT.format(method, url), - RumErrorSource.NETWORK, - throwable - ) - } - - private fun getBodyLength(response: Response?): Long? { - val body = response?.peekBody(MAX_BODY_PEEK) - val contentLength = body?.contentLength() - return if (contentLength == 0L) null else contentLength - } - - // endregion - - companion object { - - internal const val ERROR_MSG_FORMAT = "OkHttp request error %s %s" - internal val xhrMethods = arrayOf("POST", "PUT", "DELETE") - - // We need to limit this value as the body will be loaded in memory - private const val MAX_BODY_PEEK: Long = 32 * 1024L * 1024L - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt deleted file mode 100644 index e13b90fbcc..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ /dev/null @@ -1,217 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal - -import android.app.ActivityManager -import android.content.Context -import android.os.Build -import android.os.Process -import com.datadog.android.DatadogConfig -import com.datadog.android.DatadogEndpoint -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.net.GzipRequestInterceptor -import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider -import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.privacy.NoOpConsentProvider -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.core.internal.system.BroadcastReceiverSystemInfoProvider -import com.datadog.android.core.internal.system.NoOpSystemInfoProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.core.internal.time.KronosTimeProvider -import com.datadog.android.core.internal.time.LoggingSyncListener -import com.datadog.android.core.internal.time.NoOpTimeProvider -import com.datadog.android.core.internal.time.TimeProvider -import com.datadog.android.log.internal.user.DatadogUserInfoProvider -import com.datadog.android.log.internal.user.MutableUserInfoProvider -import com.datadog.android.log.internal.user.NoOpMutableUserInfoProvider -import com.datadog.android.privacy.TrackingConsent -import com.lyft.kronos.AndroidClockFactory -import com.lyft.kronos.KronosClock -import java.lang.ref.WeakReference -import java.util.concurrent.ExecutorService -import java.util.concurrent.LinkedBlockingDeque -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.ThreadPoolExecutor -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import okhttp3.ConnectionSpec -import okhttp3.OkHttpClient -import okhttp3.Protocol - -internal object CoreFeature { - - // region Constants - - internal val NETWORK_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(45) - private val THREAD_POOL_MAX_KEEP_ALIVE_MS = TimeUnit.SECONDS.toMillis(5) - private const val CORE_DEFAULT_POOL_SIZE = 1 // Only one thread will be kept alive - - // endregion - - internal val initialized = AtomicBoolean(false) - internal var contextRef: WeakReference = WeakReference(null) - internal var networkInfoProvider: NetworkInfoProvider = NoOpNetworkInfoProvider() - internal var systemInfoProvider: SystemInfoProvider = NoOpSystemInfoProvider() - internal var timeProvider: TimeProvider = NoOpTimeProvider() - internal var firstPartyHostDetector = FirstPartyHostDetector(emptyList()) - - internal var userInfoProvider: MutableUserInfoProvider = NoOpMutableUserInfoProvider() - - internal var okHttpClient: OkHttpClient = OkHttpClient.Builder().build() - internal lateinit var kronosClock: KronosClock - - internal var packageName: String = "" - internal var packageVersion: String = "" - internal var serviceName: String = "" - internal var isMainProcess: Boolean = true - internal var envName: String = "" - - internal lateinit var dataUploadScheduledExecutor: ScheduledThreadPoolExecutor - internal lateinit var dataPersistenceExecutorService: ExecutorService - internal var trackingConsentProvider: ConsentProvider = NoOpConsentProvider() - - fun initialize( - appContext: Context, - consent: TrackingConsent, - config: DatadogConfig.CoreConfig - ) { - if (initialized.get()) { - return - } - - kronosClock = AndroidClockFactory.createKronosClock( - appContext, - ntpHosts = listOf( - DatadogEndpoint.NTP_0, - DatadogEndpoint.NTP_1, - DatadogEndpoint.NTP_2, - DatadogEndpoint.NTP_3 - ), - cacheExpirationMs = TimeUnit.MINUTES.toMillis(30), - minWaitTimeBetweenSyncMs = TimeUnit.MINUTES.toMillis(5), - syncListener = LoggingSyncListener() - ).apply { syncInBackground() } - - trackingConsentProvider = TrackingConsentProvider(consent) - serviceName = config.serviceName ?: appContext.packageName - contextRef = WeakReference(appContext) - isMainProcess = resolveIsMainProcess(appContext) - envName = config.envName - - readApplicationInformation(appContext) - - setupInfoProviders(appContext) - - setupOkHttpClient(config.needsClearTextHttp) - dataUploadScheduledExecutor = ScheduledThreadPoolExecutor(CORE_DEFAULT_POOL_SIZE) - dataPersistenceExecutorService = - ThreadPoolExecutor( - CORE_DEFAULT_POOL_SIZE, - Runtime.getRuntime().availableProcessors(), - THREAD_POOL_MAX_KEEP_ALIVE_MS, - TimeUnit.MILLISECONDS, - LinkedBlockingDeque() - ) - - firstPartyHostDetector = FirstPartyHostDetector(config.hosts) - initialized.set(true) - } - - fun stop() { - if (initialized.get()) { - contextRef.get()?.let { - networkInfoProvider.unregister(it) - systemInfoProvider.unregister(it) - } - contextRef.clear() - - trackingConsentProvider.unregisterAllCallbacks() - trackingConsentProvider = NoOpConsentProvider() - timeProvider = NoOpTimeProvider() - systemInfoProvider = NoOpSystemInfoProvider() - networkInfoProvider = NoOpNetworkInfoProvider() - userInfoProvider = NoOpMutableUserInfoProvider() - firstPartyHostDetector = FirstPartyHostDetector(emptyList()) - serviceName = "" - packageName = "" - packageVersion = "" - shutDownExecutors() - initialized.set(false) - } - } - - // region Internal - - private fun shutDownExecutors() { - dataUploadScheduledExecutor.shutdownNow() - dataPersistenceExecutorService.shutdownNow() - } - - @Suppress("DEPRECATION") - private fun readApplicationInformation( - appContext: Context - ) { - packageName = appContext.packageName - packageVersion = appContext.packageManager.getPackageInfo(packageName, 0).let { - it.versionName ?: it.versionCode.toString() - } - } - - private fun setupInfoProviders(appContext: Context) { - // Time Provider - timeProvider = KronosTimeProvider(kronosClock) - - // System Info Provider - systemInfoProvider = BroadcastReceiverSystemInfoProvider() - systemInfoProvider.register(appContext) - - // Network Info Provider - networkInfoProvider = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - CallbackNetworkInfoProvider() - } else { - BroadcastReceiverNetworkInfoProvider() - } - networkInfoProvider.register(appContext) - - // User Info Provider - userInfoProvider = DatadogUserInfoProvider() - } - - private fun setupOkHttpClient(needsClearTextHttp: Boolean) { - val connectionSpec = when { - needsClearTextHttp -> ConnectionSpec.CLEARTEXT - Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> ConnectionSpec.RESTRICTED_TLS - else -> ConnectionSpec.MODERN_TLS - } - - okHttpClient = OkHttpClient.Builder() - .addInterceptor(GzipRequestInterceptor()) - .callTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .writeTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) - .connectionSpecs(listOf(connectionSpec)) - .build() - } - - private fun resolveIsMainProcess(appContext: Context): Boolean { - val currentProcessId = Process.myPid() - val manager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager - val currentProcess = manager?.runningAppProcesses?.firstOrNull { - it.pid == currentProcessId - } - return if (currentProcess == null) { - true - } else { - appContext.packageName == currentProcess.processName - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt deleted file mode 100644 index 506b40503b..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt +++ /dev/null @@ -1,38 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal - -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.DatadogPluginConfig - -internal abstract class SdkFeature { - private var featurePlugins: MutableList = mutableListOf() - - protected fun registerPlugins( - plugins: List, - config: DatadogPluginConfig, - trackingConsentProvider: ConsentProvider - ) { - this.featurePlugins = plugins.toMutableList() - plugins.forEach { - it.register(config) - trackingConsentProvider.registerCallback(it) - } - } - - protected fun unregisterPlugins() { - featurePlugins.forEach { - it.unregister() - } - featurePlugins.clear() - } - - fun getPlugins(): List { - return featurePlugins - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/constraints/DataConstraints.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/constraints/DataConstraints.kt deleted file mode 100644 index 185e8b51dc..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/constraints/DataConstraints.kt +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.constraints - -/** - * This interface allows sanitizing logs locally before uploading them to the servers. - */ -internal interface DataConstraints { - - fun validateAttributes( - attributes: Map, - keyPrefix: String? = null, - attributesGroupName: String? = null - ): Map - - fun validateTags(tags: List): List -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/constraints/DatadogDataConstraints.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/constraints/DatadogDataConstraints.kt deleted file mode 100644 index e33baf3fee..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/constraints/DatadogDataConstraints.kt +++ /dev/null @@ -1,150 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.constraints - -import com.datadog.android.core.internal.utils.devLogger -import java.util.Locale - -internal typealias StringTransform = (String) -> String? - -internal class DatadogDataConstraints : DataConstraints { - - // region DataConstraints - - override fun validateTags(tags: List): List { - val convertedTags = tags.mapNotNull { - val tag = convertTag(it) - if (tag == null) { - devLogger.e("\"$it\" is an invalid tag, and was ignored.") - } else if (tag != it) { - devLogger.w("tag \"$it\" was modified to \"$tag\" to match our constraints.") - } - tag - } - val discardedCount = convertedTags.size - MAX_TAG_COUNT - if (discardedCount > 0) { - devLogger.w("too many tags were added, $discardedCount had to be discarded.") - } - return convertedTags.take(MAX_TAG_COUNT) - } - - override fun validateAttributes( - attributes: Map, - keyPrefix: String?, - attributesGroupName: String? - ): Map { - - // prefix = "a.b" => dotCount = 1+1 ("a.b." + key) - val prefixDotCount = keyPrefix?.let { it.count { character -> character == '.' } + 1 } ?: 0 - val convertedAttributes = attributes.mapNotNull { - // We need this in case the attributes are added from JAVA code and a null key may be - // passed. - if (it.key == null) { - devLogger.e("\"$it\" is an invalid attribute, and was ignored.") - null - } - val key = convertAttributeKey(it.key, prefixDotCount) - if (key != it.key) { - devLogger.w( - "Key \"${it.key}\" " + - "was modified to \"$key\" to match our constraints." - ) - } - key to it.value - } - val discardedCount = convertedAttributes.size - MAX_ATTR_COUNT - if (discardedCount > 0) { - val warningMessage = resolveDiscardedAttrsWarning( - attributesGroupName, - discardedCount - ) - devLogger.w(warningMessage) - } - return convertedAttributes.take(MAX_ATTR_COUNT).toMap() - } - - private fun resolveDiscardedAttrsWarning( - attributesGroupName: String?, - discardedCount: Int - ): String { - return if (attributesGroupName != null) { - "Too many attributes were added for [$attributesGroupName], " + - "$discardedCount had to be discarded." - } else { - "Too many attributes were added, " + - "$discardedCount had to be discarded." - } - } - - // endregion - - // region Internal/Tag - - private val tagTransforms = listOf( - // Tags must be lowercase - { it.toLowerCase(Locale.US) }, - // Tags must start with a letter - { if (it.get(0) !in 'a'..'z') null else it }, - // Tags convert illegal characters to underscode - { it.replace(Regex("[^a-z0-9_:./-]"), "_") }, - // Tags cannot end with a colon - { if (it.endsWith(':')) it.substring(0, it.lastIndex) else it }, - // Tags can be up to 200 characters long - { if (it.length > MAX_TAG_LENGTH) it.substring(0, MAX_TAG_LENGTH) else it }, - // Dismiss tags with reserved keys - { if (isKeyReserved(it)) null else it } - ) - - private fun convertTag(rawTag: String?): String? { - return tagTransforms.fold(rawTag) { tag, transform -> - if (tag == null) null else transform.invoke(tag) - } - } - - private fun isKeyReserved(tag: String): Boolean { - val firstColon = tag.indexOf(':') - return if (firstColon > 0) { - val key = tag.substring(0, firstColon) - key in reservedTagKeys - } else { - false - } - } - - // endregion - - // region Internal/Attribute - - private fun convertAttributeKey(rawKey: String, prefixDotCount: Int): String { - var dotCount = prefixDotCount - val mapped = rawKey.map { - if (it == '.') { - dotCount++ - if (dotCount > MAX_DEPTH_LEVEL) '_' else it - } else it - } - return String(mapped.toCharArray()) - } - - // endregion - - companion object { - - private const val MAX_TAG_LENGTH = 200 - private const val MAX_TAG_COUNT = 100 - - private const val MAX_ATTR_COUNT = 128 - private const val MAX_DEPTH_LEVEL = 9 - - private val reservedTagKeys = setOf( - "host", - "device", - "source", - "service" - ) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/DataMigrator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/DataMigrator.kt deleted file mode 100644 index aaf906ae54..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/DataMigrator.kt +++ /dev/null @@ -1,12 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data - -internal interface DataMigrator { - - fun migrateData() -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Orchestrator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Orchestrator.kt deleted file mode 100644 index c537b544e3..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Orchestrator.kt +++ /dev/null @@ -1,22 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data - -import java.io.File - -internal interface Orchestrator { - - @Throws(SecurityException::class) - fun getWritableFile(itemSize: Int): File? - - @Throws(SecurityException::class) - fun getReadableFile(excludeFileNames: Set): File? - - fun getAllFiles(): Array - - fun reset() -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Reader.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Reader.kt deleted file mode 100644 index dea16e1a85..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Reader.kt +++ /dev/null @@ -1,29 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data - -import com.datadog.android.core.internal.data.file.Batch -import com.datadog.tools.annotation.NoOpImplementation - -/** - * Reads logs from a persistent location, when they can be sent. - * @see [Writer] - */ -@NoOpImplementation -internal interface Reader { - - fun readNextBatch(): Batch? - - /** - * Marks that a batch couldn't be set and should be retried later. - */ - fun releaseBatch(batchId: String) - - fun dropBatch(batchId: String) - - fun dropAllBatches() -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Writer.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Writer.kt deleted file mode 100644 index 544a07be5a..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/Writer.kt +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data - -import com.datadog.tools.annotation.NoOpImplementation - -/** - * Writes a log to a persistent location, for them to be sent at a later time (undefined). - * @see [Reader] - */ -@NoOpImplementation -internal interface Writer { - - fun write(model: T) - - fun write(models: List) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/DeferredWriter.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/DeferredWriter.kt deleted file mode 100644 index c504a8e846..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/DeferredWriter.kt +++ /dev/null @@ -1,91 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import com.datadog.android.core.internal.data.DataMigrator -import com.datadog.android.core.internal.data.Writer -import java.util.LinkedList -import java.util.concurrent.ExecutorService -import java.util.concurrent.atomic.AtomicBoolean - -internal class DeferredWriter( - private val writer: Writer, - private val executorService: ExecutorService, - dataMigrator: DataMigrator? = null -) : Writer { - - private val dataMigrated: AtomicBoolean = AtomicBoolean(false) - private val messagesQueue: LinkedList = LinkedList() - - init { - if (dataMigrator != null) { - executorService.submit( - Runnable { - dataMigrator.migrateData() - dataMigrated.set(true) -// we make sure we consume everything from the message queue - synchronized(messagesQueue) { - while (messagesQueue.isNotEmpty()) { - messagesQueue.remove().run() - } - } - } - ) - } else { - dataMigrated.set(true) - } - } - - // region Writer - - override fun write(model: T) { - handleRunnable( - Runnable { - writer.write(model) - } - ) - } - - override fun write(models: List) { - handleRunnable( - Runnable { - writer.write(models) - } - ) - } - - // endregion - - // region internal - - private fun handleRunnable(runnable: Runnable) { - if (dataMigrated.get()) { - tryToConsumeQueue() - executorService.submit(runnable) - } else { - addToQueue(runnable) - } - } - - private fun addToQueue(runnable: Runnable) { - synchronized(messagesQueue) { - messagesQueue.add(runnable) - } - } - - private fun tryToConsumeQueue() { - if (messagesQueue.isNotEmpty()) { - synchronized(messagesQueue) { - while (messagesQueue.isNotEmpty()) { - executorService.submit(messagesQueue.remove()) - } - } - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileExtensions.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileExtensions.kt deleted file mode 100644 index 8b441955f4..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileExtensions.kt +++ /dev/null @@ -1,36 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import com.datadog.android.core.internal.utils.sdkLogger -import java.io.File - -internal fun File.readBytes(withPrefix: CharSequence, withSuffix: CharSequence): ByteArray = - inputStream().use { input -> - val length = this.length() - if (length > Int.MAX_VALUE) { - sdkLogger.i("We could not read the file $this because it was too big to fit in memory") - return ByteArray(0) - } - var offset = withPrefix.length // start from the prefix - var remaining = length.toInt() - val result = ByteArray(remaining + withPrefix.length + withSuffix.length) - for (i in 0 until offset) { - result[i] = withPrefix[i].toByte() - } - while (remaining > 0) { - val read = input.read(result, offset, remaining) - if (read < 0) break - remaining -= read - offset += read - } - for (j in 0 until withSuffix.length) { - result[j + offset] = withSuffix[j].toByte() - } - offset += withSuffix.length // adding the last character (suffix) - if (remaining == 0) result else result.copyOf(offset) - } diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileFilter.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileFilter.kt deleted file mode 100644 index c8f7a61122..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileFilter.kt +++ /dev/null @@ -1,23 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import java.io.File -import java.io.FileFilter - -internal class FileFilter : FileFilter { - - override fun accept(file: File?): Boolean { - return file != null && - file.isFile && - file.name.matches(logFileNameRegex) - } - - companion object { - private val logFileNameRegex = Regex("\\d+") - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileHandler.kt deleted file mode 100644 index c3cb23e21f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileHandler.kt +++ /dev/null @@ -1,87 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import com.datadog.android.core.internal.utils.sdkLogger -import java.io.File -import java.lang.NullPointerException - -internal class FileHandler { - - // region FileHandler - - @SuppressWarnings("TooGenericExceptionCaught") - fun deleteFileOrDirectory(source: File): Boolean { - return try { - source.deleteRecursively() - } catch (e: Throwable) { - sdkLogger.e( - "Unable to clear the file at [${source.absolutePath}]", - e - ) - false - } - } - - fun moveFiles(sourceDirectory: File, destinationDirectory: File): Boolean { - if (!sourceDirectory.exists()) { - sdkLogger.w( - "There were no files to move. " + - "There is no directory at this path: [$sourceDirectory]" - ) - return true - } - - if (!sourceDirectory.isDirectory) { - sdkLogger.w( - "There were no files to move." + - "[$sourceDirectory] is not a directory." - ) - return true - } - destinationDirectory.mkdirs() - - val files = sourceDirectory.listFiles() - if (files == null || files.isEmpty()) { - return true - } - return files - .map { - moveFile(destinationDirectory, it) - }.reduce { overallSuccess, success -> - overallSuccess && success - } - } - - // endregion - - // region Internal - - @SuppressWarnings("TooGenericExceptionCaught") - private fun moveFile(destinationDirectory: File, file: File): Boolean { - val newFile = File(destinationDirectory, file.name) - return try { - file.renameTo(newFile) - } catch (e: SecurityException) { - sdkLogger.e( - "Unable to move file: [${file.absolutePath}] " + - "to new file: [${newFile.absolutePath}]", - e - ) - false - } catch (e: NullPointerException) { - sdkLogger.e( - "Unable to move file: [${file.absolutePath}] " + - "to new file: [${newFile.absolutePath}]", - e - ) - false - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileOrchestrator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileOrchestrator.kt deleted file mode 100644 index 544141ba1c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileOrchestrator.kt +++ /dev/null @@ -1,162 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.utils.sdkLogger -import java.io.File -import java.io.FileFilter - -internal class FileOrchestrator( - internal val rootDirectory: File, - internal val filePersistenceConfig: FilePersistenceConfig -) : Orchestrator { - - private val fileFilter: FileFilter = FileFilter() - - internal var previousFile: File? = null - internal var previousFileLogCount: Int = 0 - - // Offset the recent threshold for read and write to avoid conflicts - // Arbitrary offset as 5% of the threshold - private val recentWriteDelayMs = - filePersistenceConfig.recentDelayMs - (filePersistenceConfig.recentDelayMs / 20) - private val recentReadDelayMs = - filePersistenceConfig.recentDelayMs + (filePersistenceConfig.recentDelayMs / 20) - - override fun reset() { - previousFile = null - previousFileLogCount = 0 - } - - @Throws(SecurityException::class) - override fun getWritableFile(itemSize: Int): File? { - if (!isRootValid()) { - return null - } - - val files = rootDirectory.listFiles(fileFilter).orEmpty().sorted() - - deleteBigFiles(files) - - val lastFile = files.lastOrNull() - val lastKnownFile = previousFile - val lastKnownFileCount = previousFileLogCount - - // regarding the (lastKnownFile == lastFile) check, it can fail for 3 reasons : - // 1. the last file was written during a previous session (lastKnownFile == null) - // 2. something else created a more recent file in the folder - // 3. the lastKnownFile was deleted from the system - // In any case, we don't know the log count, so to be safe, we create a new log file. - return if (lastFile != null && lastKnownFile == lastFile) { - val newSize = lastFile.length() + itemSize - val fileHasRoomForMore = newSize < filePersistenceConfig.maxBatchSize - val fileIsRecentEnough = isFileRecent(lastFile, recentWriteDelayMs) - val fileHasSlotForMore = (lastKnownFileCount < filePersistenceConfig.maxItemsPerBatch) - - if (fileHasRoomForMore && fileIsRecentEnough && fileHasSlotForMore) { - previousFileLogCount = lastKnownFileCount + 1 - lastFile - } else { - newFile() - } - } else { - newFile() - } - } - - @Throws(SecurityException::class) - override fun getReadableFile(excludeFileNames: Set): File? { - if (!isRootValid()) { - return null - } - - val files = rootDirectory.listFiles(fileFilter).orEmpty().sorted() - - deleteObsoleteFiles(files) - - val nextLogFile = files.firstOrNull { - (it.name !in excludeFileNames) && (it.exists()) - } - - return if (nextLogFile == null) { - null - } else { - if (isFileRecent(nextLogFile, recentReadDelayMs)) { - null - } else { - nextLogFile - } - } - } - - override fun getAllFiles(): Array { - return rootDirectory.listFiles(fileFilter).orEmpty() - } - - // endregion - - // region Internal - - private fun isRootValid(): Boolean = if (!rootDirectory.exists()) { - rootDirectory.mkdirs() - } else { - rootDirectory.isDirectory - } - - private fun newFile(): File { - val newFileName = System.currentTimeMillis().toString() - val newFile = File(rootDirectory, newFileName) - previousFile = newFile - previousFileLogCount = 1 - return newFile - } - - private fun isFileRecent(file: File, recentDelayMs: Long): Boolean { - val now = System.currentTimeMillis() - val fileTimestamp = file.name.toLong() - return fileTimestamp >= (now - recentDelayMs) - } - - private fun deleteObsoleteFiles(files: List) { - val threshold = System.currentTimeMillis() - filePersistenceConfig.oldFileThreshold - files - .asSequence() - .filter { it.name.toLong() < threshold } - .forEach { it.delete() } - } - - private fun deleteBigFiles(files: List) { - val sizeOnDisk = files.fold(0L) { total, file -> - total + file.length() - } - val maxDiskSpace = filePersistenceConfig.maxDiskSpace - val sizeToFree = sizeOnDisk - maxDiskSpace - if (sizeToFree > 0) { - sdkLogger.w( - "Too much disk space used ($sizeOnDisk / $maxDiskSpace): " + - "cleaning up to free $sizeToFree bytes…" - ) - files.asSequence() - .fold(sizeToFree) { remainingSizeToFree, file -> - if (remainingSizeToFree > 0) { - val fileSize = file.length() - if (file.delete()) { - remainingSizeToFree - fileSize - } else { - remainingSizeToFree - } - } else { - remainingSizeToFree - } - } - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileReader.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileReader.kt deleted file mode 100644 index 87d933a459..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/FileReader.kt +++ /dev/null @@ -1,124 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.utils.sdkLogger -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException - -internal class FileReader( - internal val fileOrchestrator: Orchestrator, - private val dataDirectory: File, - private val prefix: CharSequence = "", - private val suffix: CharSequence = "" -) : Reader { - - private val lockedFiles: MutableSet = mutableSetOf() - - // region LogReader - - override fun readNextBatch(): Batch? { - val (file, data) = readNextFile() - - return if (file == null) { - null - } else { - Batch( - file.name, - data - ) - } - } - - override fun releaseBatch(batchId: String) { - sdkLogger.i("releaseBatch $batchId") - synchronized(lockedFiles) { - lockedFiles.remove(batchId) - } - } - - override fun dropBatch(batchId: String) { - sdkLogger.i("dropBatch $batchId") - val fileToDelete = File(dataDirectory, batchId) - if (deleteFile(fileToDelete)) { - releaseBatch(batchId) - } - } - - override fun dropAllBatches() { - sdkLogger.i("dropAllBatches") - fileOrchestrator - .getAllFiles() - .forEach { - if (deleteFile(it)) { - releaseBatch(it.name) - } - } - } - - // endregion - - // region Internal - - private fun readNextFile(): Pair { - val file = lockAndGetFile() - return if (file != null) { - val data = try { - file.readBytes(withPrefix = prefix, withSuffix = suffix) - } catch (e: FileNotFoundException) { - sdkLogger.e("Couldn't create an input stream from file ${file?.path}", e) - ByteArray(0) - } catch (e: IOException) { - sdkLogger.e("Couldn't read messages from file ${file?.path}", e) - ByteArray(0) - } - file to data - } else { - file to ByteArray(0) - } - } - - private fun lockAndGetFile(): File? { - var file: File? = null - try { - synchronized(lockedFiles) { - val readFile = fileOrchestrator.getReadableFile(lockedFiles.toSet()) - if (readFile != null) { - lockedFiles.add(readFile.name) - } - file = readFile - } - } catch (e: SecurityException) { - sdkLogger.e("Couldn't access file ${file?.path}", e) - ByteArray(0) - } catch (e: OutOfMemoryError) { - sdkLogger.e("Couldn't read file ${file?.path} (not enough memory)", e) - ByteArray(0) - } - return file - } - - private fun deleteFile(fileToDelete: File): Boolean { - if (fileToDelete.exists()) { - if (fileToDelete.delete()) { - sdkLogger.d("File ${fileToDelete.path} deleted") - return true - } else { - sdkLogger.e("Error deleting file ${fileToDelete.path}") - } - } else { - sdkLogger.w("file ${fileToDelete.path} does not exist") - } - - return false - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/ImmediateFileWriter.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/ImmediateFileWriter.kt deleted file mode 100644 index c352adf3b9..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/file/ImmediateFileWriter.kt +++ /dev/null @@ -1,113 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.PayloadDecoration -import com.datadog.android.core.internal.domain.Serializer -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.core.internal.utils.sdkLogger -import com.datadog.android.core.internal.utils.use -import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException - -internal class ImmediateFileWriter( - internal val fileOrchestrator: Orchestrator, - private val serializer: Serializer, - separator: CharSequence = PayloadDecoration.JSON_ARRAY_DECORATION.separator -) : Writer { - - private val separatorBytes = separator.toString().toByteArray(Charsets.UTF_8) - - // region Writer - - override fun write(model: T) { - consume(model) - } - - override fun write(models: List) { - models.forEach { - consume(it) - } - } - - // endregion - - // region Internal - - @SuppressWarnings("TooGenericExceptionCaught") - private fun consume(model: T) { - val data = try { - serializer.serialize(model) - } catch (e: Throwable) { - sdkLogger.w("Unable to serialize ${model.javaClass.simpleName}", e) - return - } - - if (data.length >= MAX_ITEM_SIZE) { - devLogger.e("Unable to persist data, serialized size is too big\n$data") - } else { - synchronized(this) { - writeData(data) - } - } - } - - private fun writeData(data: String) { - val dataAsByteArray = data.toByteArray(Charsets.UTF_8) - val file = try { - fileOrchestrator.getWritableFile(dataAsByteArray.size) - } catch (e: SecurityException) { - sdkLogger.e("Unable to access batch file directory", e) - null - } - - if (file != null) { - writeDataToFile(file, dataAsByteArray) - } else { - sdkLogger.e("Could not get a valid file") - } - } - - private fun writeDataToFile(file: File, dataAsByteArray: ByteArray) { - try { - val outputStream = FileOutputStream(file, true) - outputStream.use { stream -> - lockFileAndWriteData(stream, file, dataAsByteArray) - } - } catch (e: IllegalStateException) { - sdkLogger.e("Exception when trying to lock the file: [${file.canonicalPath}] ", e) - } catch (e: FileNotFoundException) { - sdkLogger.e("Couldn't create an output stream to file ${file.path}", e) - } catch (e: IOException) { - sdkLogger.e("Exception when trying to write data to: [${file.canonicalPath}] ", e) - } - } - - private fun lockFileAndWriteData( - stream: FileOutputStream, - file: File, - dataAsByteArray: ByteArray - ) { - stream.channel.lock().use { - if (file.length() > 0) { - stream.write(separatorBytes + dataAsByteArray) - } else { - stream.write(dataAsByteArray) - } - } - } - - // endregion - - companion object { - private const val MAX_ITEM_SIZE = 256 * 1024 // 256 Kb - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnable.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnable.kt deleted file mode 100644 index f65fde2919..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnable.kt +++ /dev/null @@ -1,117 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.upload - -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.data.file.Batch -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.UploadStatus -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.system.SystemInfo -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.core.internal.utils.sdkLogger -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.TimeUnit -import kotlin.math.max -import kotlin.math.min - -internal class DataUploadRunnable( - private val threadPoolExecutor: ScheduledThreadPoolExecutor, - private val reader: Reader, - private val dataUploader: DataUploader, - private val networkInfoProvider: NetworkInfoProvider, - private val systemInfoProvider: SystemInfoProvider -) : UploadRunnable { - - private var currentDelayInterval = DEFAULT_DELAY_MS - - // region Runnable - - override fun run() { - val batch = if (isNetworkAvailable() && isSystemReady()) { - reader.readNextBatch() - } else null - - if (batch != null) { - consumeBatch(batch) - } else { - increaseInterval() - } - - scheduleNextUpload() - } - - // endregion - - // region Internal - - private fun isNetworkAvailable(): Boolean { - val networkInfo = networkInfoProvider.getLatestNetworkInfo() - return networkInfo.connectivity != NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED - } - - private fun isSystemReady(): Boolean { - val systemInfo = systemInfoProvider.getLatestSystemInfo() - val batteryFullOrCharging = systemInfo.batteryStatus in batteryFullOrChargingStatus - val batteryLevel = systemInfo.batteryLevel - val powerSaveMode = systemInfo.powerSaveMode - return (batteryFullOrCharging || batteryLevel > LOW_BATTERY_THRESHOLD) && !powerSaveMode - } - - private fun scheduleNextUpload() { - threadPoolExecutor.remove(this) - threadPoolExecutor.schedule(this, currentDelayInterval, TimeUnit.MILLISECONDS) - } - - private fun consumeBatch(batch: Batch) { - val batchId = batch.id - sdkLogger.i("Sending batch $batchId") - val status = dataUploader.upload(batch.data) - status.logStatus(dataUploader.javaClass.simpleName, batch.data.size) - if (status in droppableBatchStatus) { - reader.dropBatch(batchId) - decreaseInterval() - } else { - reader.releaseBatch(batchId) - increaseInterval() - } - } - - private fun decreaseInterval() { - currentDelayInterval = max(MIN_DELAY_MS, currentDelayInterval * DECREASE_PERCENT / 100) - } - - private fun increaseInterval() { - currentDelayInterval = min(MAX_DELAY_MS, currentDelayInterval * INCREASE_PERCENT / 100) - } - - // endregion - - companion object { - - private val droppableBatchStatus = setOf( - UploadStatus.SUCCESS, - UploadStatus.HTTP_REDIRECTION, - UploadStatus.HTTP_CLIENT_ERROR, - UploadStatus.UNKNOWN_ERROR - ) - - private val batteryFullOrChargingStatus = setOf( - SystemInfo.BatteryStatus.CHARGING, - SystemInfo.BatteryStatus.FULL - ) - - private const val LOW_BATTERY_THRESHOLD = 10 - - const val DEFAULT_DELAY_MS = 5000L // 5 seconds - const val MIN_DELAY_MS = 1000L // 1 second - const val MAX_DELAY_MS = DEFAULT_DELAY_MS * 4 // 20 seconds - const val DECREASE_PERCENT = 90 - const val INCREASE_PERCENT = 110 - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadScheduler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadScheduler.kt deleted file mode 100644 index 0ce7465119..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataUploadScheduler.kt +++ /dev/null @@ -1,44 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.upload - -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.TimeUnit - -internal class DataUploadScheduler( - reader: Reader, - dataUploader: DataUploader, - networkInfoProvider: NetworkInfoProvider, - systemInfoProvider: SystemInfoProvider, - private val scheduledThreadPoolExecutor: ScheduledThreadPoolExecutor -) : UploadScheduler { - - private val runnable = - DataUploadRunnable( - scheduledThreadPoolExecutor, - reader, - dataUploader, - networkInfoProvider, - systemInfoProvider - ) - - override fun startScheduling() { - scheduledThreadPoolExecutor.schedule( - runnable, - DataUploadRunnable.DEFAULT_DELAY_MS, - TimeUnit.MILLISECONDS - ) - } - - override fun stopScheduling() { - scheduledThreadPoolExecutor.remove(runnable) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadWorker.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadWorker.kt deleted file mode 100644 index 25a7213119..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/data/upload/UploadWorker.kt +++ /dev/null @@ -1,99 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.upload - -import android.content.Context -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.datadog.android.Datadog -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.data.file.Batch -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.UploadStatus -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.core.internal.utils.sdkLogger -import com.datadog.android.error.internal.CrashReportsFeature -import com.datadog.android.log.internal.LogsFeature -import com.datadog.android.tracing.internal.TracesFeature - -internal class UploadWorker( - appContext: Context, - workerParams: WorkerParameters -) : Worker(appContext, workerParams) { - - // region Worker - - override fun doWork(): Result { - if (!Datadog.isInitialized()) { - devLogger.e(Datadog.MESSAGE_NOT_INITIALIZED) - return Result.success() - } - - // Upload Crash reports - uploadAllBatches( - CrashReportsFeature.persistenceStrategy.getReader(), - CrashReportsFeature.uploader - ) - - // Upload Logs - uploadAllBatches( - LogsFeature.persistenceStrategy.getReader(), - LogsFeature.uploader - ) - - // Upload Traces - uploadAllBatches( - TracesFeature.persistenceStrategy.getReader(), - TracesFeature.uploader - ) - - return Result.success() - } - - private fun uploadAllBatches( - reader: Reader, - uploader: DataUploader - ) { - val failedBatches = mutableListOf() - var batch: Batch? - do { - batch = reader.readNextBatch() - if (batch != null) { - if (consumeBatch(batch, uploader)) { - reader.dropBatch(batch.id) - } else { - failedBatches.add(batch) - } - } - } while (batch != null) - - failedBatches.forEach { - reader.releaseBatch(it.id) - } - } - - // endregion - - // region Internal - - private fun consumeBatch( - batch: Batch, - uploader: DataUploader - ): Boolean { - val batchId = batch.id - sdkLogger.i("$TAG: Sending batch $batchId") - val status = uploader.upload(batch.data) - status.logStatus(uploader.javaClass.simpleName, batch.data.size) - return status == UploadStatus.SUCCESS - } - - // endregion - - companion object { - private const val TAG = "UploadWorker" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/FilePersistenceConfig.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/FilePersistenceConfig.kt deleted file mode 100644 index 7fb58af9df..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/FilePersistenceConfig.kt +++ /dev/null @@ -1,23 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -internal data class FilePersistenceConfig( - val recentDelayMs: Long = MAX_DELAY_BETWEEN_MESSAGES_MS, - val maxBatchSize: Long = MAX_BATCH_SIZE, - val maxItemsPerBatch: Int = MAX_ITEMS_PER_BATCH, - val oldFileThreshold: Long = OLD_FILE_THRESHOLD, - val maxDiskSpace: Long = MAX_DISK_SPACE -) { - companion object { - internal const val MAX_BATCH_SIZE: Long = 4 * 1024 * 1024 // 4 MB - internal const val MAX_ITEMS_PER_BATCH: Int = 500 - internal const val OLD_FILE_THRESHOLD: Long = 18L * 60L * 60L * 1000L // 18 hours - internal const val MAX_DISK_SPACE: Long = 128 * MAX_BATCH_SIZE // 512 MB - internal const val MAX_DELAY_BETWEEN_MESSAGES_MS = 5000L - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/FilePersistenceStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/FilePersistenceStrategy.kt deleted file mode 100644 index 3af41c943f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/FilePersistenceStrategy.kt +++ /dev/null @@ -1,80 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.data.file.FileOrchestrator -import com.datadog.android.core.internal.data.file.FileReader -import com.datadog.android.core.internal.domain.batching.ConsentAwareDataWriter -import com.datadog.android.core.internal.domain.batching.DataProcessorFactory -import com.datadog.android.core.internal.domain.batching.DefaultConsentAwareDataWriter -import com.datadog.android.core.internal.domain.batching.DefaultMigratorFactory -import com.datadog.android.core.internal.privacy.ConsentProvider -import java.io.File -import java.util.concurrent.ExecutorService - -internal open class FilePersistenceStrategy( - intermediateStorageFolder: File, - authorizedStorageFolder: File, - serializer: Serializer, - executorService: ExecutorService, - filePersistenceConfig: FilePersistenceConfig = FilePersistenceConfig(), - payloadDecoration: PayloadDecoration = PayloadDecoration.JSON_ARRAY_DECORATION, - trackingConsentProvider: ConsentProvider -) : PersistenceStrategy { - - internal val intermediateFileOrchestrator = FileOrchestrator( - intermediateStorageFolder, - filePersistenceConfig - ) - - internal val authorizedFileOrchestrator = FileOrchestrator( - authorizedStorageFolder, - filePersistenceConfig - ) - - internal val fileReader = FileReader( - authorizedFileOrchestrator, - authorizedStorageFolder, - payloadDecoration.prefix, - payloadDecoration.suffix - ) - - internal val consentAwareDataWriter: ConsentAwareDataWriter = - DefaultConsentAwareDataWriter( - consentProvider = trackingConsentProvider, - processorsFactory = DataProcessorFactory( - intermediateFileOrchestrator, - authorizedFileOrchestrator, - serializer, - payloadDecoration.separator, - executorService - ), - migratorsFactory = DefaultMigratorFactory( - intermediateStorageFolder.absolutePath, - authorizedStorageFolder.absolutePath, - executorService - ) - ) - - // region PersistenceStrategy - - override fun getWriter(): Writer { - return consentAwareDataWriter - } - - override fun getReader(): Reader { - return fileReader - } - - override fun clearAllData() { - fileReader.dropAllBatches() - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/PayloadDecoration.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/PayloadDecoration.kt deleted file mode 100644 index b242369d8e..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/PayloadDecoration.kt +++ /dev/null @@ -1,19 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -internal data class PayloadDecoration( - val prefix: CharSequence, - val suffix: CharSequence, - val separator: CharSequence -) { - - companion object { - val JSON_ARRAY_DECORATION = PayloadDecoration("[", "]", ",") - val NEW_LINE_DECORATION = PayloadDecoration("", "", "\n") - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/PersistenceStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/PersistenceStrategy.kt deleted file mode 100644 index b2722d0bd1..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/PersistenceStrategy.kt +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.data.Writer -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface PersistenceStrategy { - - fun getWriter(): Writer - - fun getReader(): Reader - - fun clearAllData() -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/Serializer.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/Serializer.kt deleted file mode 100644 index 2d7444501c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/Serializer.kt +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -/** - * The Serializer generic interface. Should be implemented by any custom serializer. - */ -internal interface Serializer { - - fun serialize(model: T): String -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/Time.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/Time.kt deleted file mode 100644 index e05b366ecd..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/Time.kt +++ /dev/null @@ -1,12 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -internal data class Time( - val timestamp: Long = System.currentTimeMillis(), - val nanoTime: Long = System.nanoTime() -) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/ConsentAwareDataWriter.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/ConsentAwareDataWriter.kt deleted file mode 100644 index 44379abf87..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/ConsentAwareDataWriter.kt +++ /dev/null @@ -1,13 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.data.Writer - -internal interface ConsentAwareDataWriter : Writer { - fun getInternalWriter(): Writer -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DataProcessorFactory.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DataProcessorFactory.kt deleted file mode 100644 index 321eb5ae06..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DataProcessorFactory.kt +++ /dev/null @@ -1,46 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.core.internal.data.file.ImmediateFileWriter -import com.datadog.android.core.internal.domain.Serializer -import com.datadog.android.core.internal.domain.batching.processors.DataProcessor -import com.datadog.android.core.internal.domain.batching.processors.DefaultDataProcessor -import com.datadog.android.core.internal.domain.batching.processors.NoOpDataProcessor -import com.datadog.android.privacy.TrackingConsent -import java.util.concurrent.ExecutorService - -internal class DataProcessorFactory( - private val intermediateFileOrchestrator: Orchestrator, - private val targetFileOrchestrator: Orchestrator, - private val serializer: Serializer, - private val separator: CharSequence, - private val executorService: ExecutorService -) { - - fun resolveProcessor(consent: TrackingConsent): DataProcessor { - return when (consent) { - TrackingConsent.PENDING -> { - intermediateFileOrchestrator.reset() - DefaultDataProcessor( - executorService, - ImmediateFileWriter(intermediateFileOrchestrator, serializer, separator) - ) - } - TrackingConsent.GRANTED -> { - DefaultDataProcessor( - executorService, - ImmediateFileWriter(targetFileOrchestrator, serializer, separator) - ) - } - else -> { - NoOpDataProcessor() - } - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DefaultConsentAwareDataWriter.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DefaultConsentAwareDataWriter.kt deleted file mode 100644 index 5b2a1eab02..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DefaultConsentAwareDataWriter.kt +++ /dev/null @@ -1,64 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.batching.processors.DataProcessor -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.privacy.TrackingConsentProviderCallback - -internal class DefaultConsentAwareDataWriter( - - consentProvider: ConsentProvider, - private val processorsFactory: DataProcessorFactory, - private val migratorsFactory: MigratorFactory -) : ConsentAwareDataWriter, TrackingConsentProviderCallback { - - private var processor: DataProcessor - - init { - consentProvider.registerCallback(this) - processor = resolveProcessor(null, consentProvider.getConsent()) - } - - // region ConsentAwareDataHandler - - @Synchronized - override fun write(model: T) { - processor.consume(model) - } - - @Synchronized - override fun write(models: List) { - processor.consume(models) - } - - @Synchronized - override fun onConsentUpdated(previousConsent: TrackingConsent, newConsent: TrackingConsent) { - processor = resolveProcessor(previousConsent, newConsent) - } - - override fun getInternalWriter(): Writer { - return processor.getWriter() - } - - // endregion - - // region Internal - - private fun resolveProcessor( - prevConsentFlag: TrackingConsent?, - newConsentFlag: TrackingConsent - ): DataProcessor { - val migrator = migratorsFactory.resolveMigrator(prevConsentFlag, newConsentFlag) - migrator.migrateData() - return processorsFactory.resolveProcessor(newConsentFlag) - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DefaultMigratorFactory.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DefaultMigratorFactory.kt deleted file mode 100644 index e1ff65ee91..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/DefaultMigratorFactory.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.domain.batching.migrators.BatchedDataMigrator -import com.datadog.android.core.internal.domain.batching.migrators.MoveDataMigrator -import com.datadog.android.core.internal.domain.batching.migrators.NoOpBatchedDataMigrator -import com.datadog.android.core.internal.domain.batching.migrators.WipeDataMigrator -import com.datadog.android.privacy.TrackingConsent -import java.util.concurrent.ExecutorService - -internal class DefaultMigratorFactory( - private val pendingFolderPath: String, - private val approvedFolderPath: String, - private val executorService: ExecutorService -) : MigratorFactory { - - override fun resolveMigrator( - prevConsentFlag: TrackingConsent?, - newConsentFlag: TrackingConsent - ): BatchedDataMigrator { - - return when (prevConsentFlag to newConsentFlag) { - TrackingConsent.PENDING to TrackingConsent.NOT_GRANTED -> { - WipeDataMigrator(pendingFolderPath, executorService) - } - TrackingConsent.PENDING to TrackingConsent.GRANTED -> { - MoveDataMigrator(pendingFolderPath, approvedFolderPath, executorService) - } - // We need this to make sure we clear the current folder when initializing the SDK - null to TrackingConsent.PENDING, - TrackingConsent.GRANTED to TrackingConsent.PENDING, - TrackingConsent.NOT_GRANTED to TrackingConsent.PENDING -> { - WipeDataMigrator(pendingFolderPath, executorService) - } - else -> NoOpBatchedDataMigrator() - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/MigratorFactory.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/MigratorFactory.kt deleted file mode 100644 index c76a67994b..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/MigratorFactory.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.domain.batching.migrators.BatchedDataMigrator -import com.datadog.android.privacy.TrackingConsent - -internal interface MigratorFactory { - fun resolveMigrator( - prevConsentFlag: TrackingConsent?, - newConsentFlag: TrackingConsent - ): BatchedDataMigrator -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/BatchedDataMigrator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/BatchedDataMigrator.kt deleted file mode 100644 index 54e6a4ca32..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/BatchedDataMigrator.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.migrators - -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface BatchedDataMigrator { - fun migrateData() -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/MoveDataMigrator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/MoveDataMigrator.kt deleted file mode 100644 index b00524261f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/MoveDataMigrator.kt +++ /dev/null @@ -1,40 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.migrators - -import com.datadog.android.core.internal.data.file.FileHandler -import com.datadog.android.core.internal.utils.retryWithDelay -import java.io.File -import java.util.concurrent.ExecutorService -import java.util.concurrent.TimeUnit - -internal class MoveDataMigrator( - internal val pendingFolderPath: String, - internal val approvedFolderPath: String, - private val executorService: ExecutorService, - private val fileHandler: FileHandler = FileHandler() -) : BatchedDataMigrator { - override fun migrateData() { - executorService.submit { - retryWithDelay( - { - fileHandler.moveFiles( - File(pendingFolderPath), - File(approvedFolderPath) - ) - }, - MAX_RETRIES, - RETRY_DELAY_IN_NANOS - ) - } - } - - companion object { - private const val MAX_RETRIES = 3 - private val RETRY_DELAY_IN_NANOS = TimeUnit.SECONDS.toNanos(1) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/WipeDataMigrator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/WipeDataMigrator.kt deleted file mode 100644 index 9405db201c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/migrators/WipeDataMigrator.kt +++ /dev/null @@ -1,37 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.migrators - -import com.datadog.android.core.internal.data.file.FileHandler -import com.datadog.android.core.internal.utils.retryWithDelay -import java.io.File -import java.util.concurrent.ExecutorService -import java.util.concurrent.TimeUnit - -internal class WipeDataMigrator( - internal val folderPath: String, - private val executorService: ExecutorService, - private val fileHandler: FileHandler = FileHandler() -) : BatchedDataMigrator { - - override fun migrateData() { - executorService.submit { - retryWithDelay( - { - fileHandler.deleteFileOrDirectory(File(folderPath)) - }, - MAX_RETRIES, - RETRY_DELAY_IN_NANOS - ) - } - } - - companion object { - private const val MAX_RETRIES = 3 - private val RETRY_DELAY_IN_NANOS = TimeUnit.SECONDS.toNanos(1) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/processors/DataProcessor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/processors/DataProcessor.kt deleted file mode 100644 index 7a39fafbfc..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/processors/DataProcessor.kt +++ /dev/null @@ -1,19 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.processors - -import com.datadog.android.core.internal.data.Writer -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface DataProcessor { - fun consume(event: T) - - fun consume(events: List) - - fun getWriter(): Writer -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/processors/DefaultDataProcessor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/processors/DefaultDataProcessor.kt deleted file mode 100644 index c1831cfa53..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/domain/batching/processors/DefaultDataProcessor.kt +++ /dev/null @@ -1,32 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.processors - -import com.datadog.android.core.internal.data.Writer -import java.util.concurrent.ExecutorService - -internal class DefaultDataProcessor( - val executorService: ExecutorService, - val dataWriter: Writer -) : DataProcessor { - - override fun consume(event: T) { - executorService.submit { - dataWriter.write(event) - } - } - - override fun consume(events: List) { - executorService.submit { - dataWriter.write(events) - } - } - - override fun getWriter(): Writer { - return dataWriter - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback.kt deleted file mode 100644 index ddf76debaf..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback.kt +++ /dev/null @@ -1,49 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.lifecycle - -import android.content.Context -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.utils.cancelUploadWorker -import com.datadog.android.core.internal.utils.triggerUploadWorker -import java.lang.ref.WeakReference - -internal class ProcessLifecycleCallback( - val networkInfoProvider: NetworkInfoProvider, - appContext: Context -) : - ProcessLifecycleMonitor.Callback { - - private val contextWeakRef = WeakReference(appContext) - - override fun onStarted() { - contextWeakRef.get()?.let { - cancelUploadWorker(it) - } - } - - override fun onResumed() { - // NO - OP - } - - override fun onStopped() { - val isOffline = ( - networkInfoProvider.getLatestNetworkInfo().connectivity - == NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED - ) - if (isOffline) { - contextWeakRef.get()?.let { - triggerUploadWorker(it) - } - } - } - - override fun onPaused() { - // NO - OP - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/DataOkHttpUploader.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/DataOkHttpUploader.kt deleted file mode 100644 index 1c8ea74c84..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/DataOkHttpUploader.kt +++ /dev/null @@ -1,139 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import android.os.Build -import com.datadog.android.BuildConfig -import com.datadog.android.core.internal.utils.sdkLogger -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody - -internal abstract class DataOkHttpUploader( - private var url: String, - private val client: OkHttpClient, - internal val contentType: String = CONTENT_TYPE_JSON -) : DataUploader { - - // region DataUploader - - @Suppress("TooGenericExceptionCaught") - override fun upload(data: ByteArray): UploadStatus { - - return try { - val request = buildRequest(data) - val call = client.newCall(request) - val response = call.execute() - sdkLogger.i( - "Response " + - "from ${url.substring(0, 32)}… " + - "code:${response.code()} " + - "body:${response.body()?.string()} " + - "headers:${response.headers()}" - ) - responseCodeToUploadStatus(response.code()) - } catch (e: Throwable) { - sdkLogger.e("unable to upload data", e) - UploadStatus.NETWORK_ERROR - } - } - - // endregion - - // region DataOkHttpUploader - - open fun setEndpoint(endpoint: String) { - this.url = endpoint - } - - abstract fun buildQueryParams(): Map - - // endregion - - // region Internal - - private fun headers(): MutableMap { - return mutableMapOf( - HEADER_UA to userAgent, - HEADER_CT to contentType - ) - } - - private val userAgent by lazy { - System.getProperty(SYSTEM_UA).let { - if (it.isNullOrBlank()) { - "Datadog/${BuildConfig.VERSION_NAME} " + - "(Linux; U; Android ${Build.VERSION.RELEASE}; " + - "${Build.MODEL} Build/${Build.ID})" - } else { - it - } - } - } - - private fun buildRequest(data: ByteArray): Request { - // add query params - val urlWithQueryParams = urlWithQueryParams() - sdkLogger.d("Sending data to POST $urlWithQueryParams") - val builder = Request.Builder() - .url(http://github.com/urlWithQueryParams) - .post(RequestBody.create(null, data)) - headers().forEach { - builder.addHeader(it.key, it.value) - sdkLogger.d("$TAG: ${it.key}: ${it.value}") - } - return builder.build() - } - - private fun urlWithQueryParams(): String { - val baseUrl = url - var firstAdded = false - return buildQueryParams() - .asSequence() - .fold( - baseUrl, - { url, entry -> - if (firstAdded) { - "$url&${entry.key}=${entry.value}" - } else { - firstAdded = true - "$url?${entry.key}=${entry.value}" - } - } - ) - } - - private fun responseCodeToUploadStatus(code: Int): UploadStatus { - return when (code) { - 403 -> UploadStatus.INVALID_TOKEN_ERROR - in 200..299 -> UploadStatus.SUCCESS - in 300..399 -> UploadStatus.HTTP_REDIRECTION - in 400..499 -> UploadStatus.HTTP_CLIENT_ERROR - in 500..599 -> UploadStatus.HTTP_SERVER_ERROR - else -> UploadStatus.UNKNOWN_ERROR - } - } - - // endregion - - companion object { - internal const val DD_SOURCE_ANDROID = "android" - - internal const val CONTENT_TYPE_JSON = "application/json" - internal const val CONTENT_TYPE_TEXT_UTF8 = "text/plain;charset=UTF-8" - - internal const val QP_BATCH_TIME = "batch_time" - internal const val QP_SOURCE = "ddsource" - - private const val HEADER_CT = "Content-Type" - private const val HEADER_UA = "User-Agent" - - const val SYSTEM_UA = "http.agent" - - private const val TAG = "DataOkHttpUploader" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/DataUploader.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/DataUploader.kt deleted file mode 100644 index 2367e422c9..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/DataUploader.kt +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface DataUploader { - - fun upload(data: ByteArray): UploadStatus -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/FirstPartyHostDetector.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/FirstPartyHostDetector.kt deleted file mode 100644 index a40ffc653f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/FirstPartyHostDetector.kt +++ /dev/null @@ -1,33 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import java.util.Locale -import okhttp3.HttpUrl - -internal class FirstPartyHostDetector( - hosts: List -) { - // As per - internal val knownHosts = hosts.map { it.toLowerCase(Locale.US) } - - fun isFirstPartyUrl(url: HttpUrl): Boolean { - val host = url.host() - return knownHosts.any { - host == it || host.endsWith(".$it") - } - } - - fun isFirstPartyUrl(url: String): Boolean { - val httpUrl = HttpUrl.parse(url) ?: return false - return isFirstPartyUrl(httpUrl) - } - - fun isEmpty(): Boolean { - return knownHosts.isEmpty() - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/GzipRequestInterceptor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/GzipRequestInterceptor.kt deleted file mode 100644 index 06c7106645..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/GzipRequestInterceptor.kt +++ /dev/null @@ -1,73 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import okhttp3.Interceptor -import okhttp3.MediaType -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.Response -import okio.BufferedSink -import okio.GzipSink -import okio.Okio - -/** - * This interceptor compresses the HTTP request body. - * - * This class uses the [GzipSink] to compress the body content. - */ -internal class GzipRequestInterceptor : Interceptor { - - // region Interceptor - - /** - * Observes, modifies, or short-circuits requests going out and the responses coming back in. - */ - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest: Request = chain.request() - val body = originalRequest.body() - - return if (body == null || originalRequest.header(HEADER_ENCODING) != null) { - chain.proceed(originalRequest) - } else { - val compressedRequest = originalRequest.newBuilder() - .header(HEADER_ENCODING, ENCODING_GZIP) - .method(originalRequest.method(), gzip(body)) - .build() - chain.proceed(compressedRequest) - } - } - - // endregion - - // region Internal - - private fun gzip(body: RequestBody): RequestBody? { - return object : RequestBody() { - override fun contentType(): MediaType? { - return body.contentType() - } - - override fun contentLength(): Long { - return -1 // We don't know the compressed length in advance! - } - - override fun writeTo(sink: BufferedSink) { - val gzipSink: BufferedSink = Okio.buffer(GzipSink(sink)) - body.writeTo(gzipSink) - gzipSink.close() - } - } - } - - // endregion - - companion object { - private const val HEADER_ENCODING = "Content-Encoding" - private const val ENCODING_GZIP = "gzip" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/RequestUniqueIdentifier.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/RequestUniqueIdentifier.kt deleted file mode 100644 index 9865becfe5..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/RequestUniqueIdentifier.kt +++ /dev/null @@ -1,26 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import okhttp3.Request -import okhttp3.internal.Util - -/** - * Generates an identifier to uniquely track requests. - */ -internal fun identifyRequest(request: Request): String { - val method = request.method() - val url = request.url() - val body = request.body() - return if (body == null || body == Util.EMPTY_REQUEST) { - "$method•$url" - } else { - val contentLength = body.contentLength() - val contentType = body.contentType() - "$method•$url•$contentLength•$contentType" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/UploadStatus.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/UploadStatus.kt deleted file mode 100644 index 31ceabeb82..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/UploadStatus.kt +++ /dev/null @@ -1,54 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import com.datadog.android.core.internal.utils.devLogger - -@Suppress("StringLiteralDuplication") -internal enum class UploadStatus { - SUCCESS, - NETWORK_ERROR, - INVALID_TOKEN_ERROR, - HTTP_REDIRECTION, - HTTP_CLIENT_ERROR, - HTTP_SERVER_ERROR, - UNKNOWN_ERROR; - - fun logStatus(context: String, byteSize: Int) { - when (this) { - NETWORK_ERROR -> devLogger.e( - "Unable to send batch [$byteSize bytes] ($context)" + - " because of a network error; we will retry later." - ) - INVALID_TOKEN_ERROR -> devLogger.e( - "Unable to send batch [$byteSize bytes] ($context)" + - " because your token is invalid. Make sure that the" + - " provided token still exists." - ) - HTTP_REDIRECTION -> devLogger.w( - "Unable to send batch [$byteSize bytes] ($context)" + - " because of a network error; we will retry later." - ) - HTTP_CLIENT_ERROR -> devLogger.e( - "Unable to send batch [$byteSize bytes] ($context)" + - " because of a processing error (possibly because of invalid data); " + - "the batch was dropped." - ) - HTTP_SERVER_ERROR -> devLogger.e( - "Unable to send batch [$byteSize bytes] ($context)" + - " because of a server processing error; we will retry later." - ) - UNKNOWN_ERROR -> devLogger.e( - "Unable to send batch [$byteSize bytes] ($context)" + - " because of an unknown error; we will retry later." - ) - SUCCESS -> devLogger.v( - "Batch [$byteSize bytes] sent successfully ($context)." - ) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt deleted file mode 100644 index 5d25dc2112..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt +++ /dev/null @@ -1,117 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net.info - -import android.annotation.TargetApi -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities -import android.os.Build -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.core.internal.utils.sdkLogger - -@TargetApi(Build.VERSION_CODES.N) -internal class CallbackNetworkInfoProvider : - ConnectivityManager.NetworkCallback(), - NetworkInfoProvider { - - private var networkInfo: NetworkInfo = NetworkInfo() - - // region NetworkCallback - - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - super.onCapabilitiesChanged(network, networkCapabilities) - sdkLogger.v("onCapabilitiesChanged $network $networkCapabilities") - - networkInfo = NetworkInfo( - connectivity = getNetworkType(networkCapabilities), - upKbps = networkCapabilities.linkUpstreamBandwidthKbps, - downKbps = networkCapabilities.linkDownstreamBandwidthKbps, - strength = getStrength(networkCapabilities) - ) - } - - override fun onLost(network: Network) { - super.onLost(network) - sdkLogger.i("onLost $network") - networkInfo = - NetworkInfo( - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED - ) - } - - // endregion - - //region NetworkInfoProvider - - override fun register(context: Context) { - val connMgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - try { - connMgr.registerDefaultNetworkCallback(this) - val activeNetwork = connMgr.activeNetwork - val activeCaps = connMgr.getNetworkCapabilities(activeNetwork) - if (activeNetwork != null && activeCaps != null) { - onCapabilitiesChanged(activeNetwork, activeCaps) - } - } catch (e: SecurityException) { - // RUMM-852 On some devices we get a SecurityException with message - // "package does not belong to 10411" - devLogger.e(ERROR_REGISTER, e) - networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_OTHER) - } - } - - override fun unregister(context: Context) { - val connMgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - try { - connMgr.unregisterNetworkCallback(this) - } catch (e: SecurityException) { - // RUMM-852 On some devices we get a SecurityException with message - // "package does not belong to 10411" - devLogger.e(ERROR_UNREGISTER, e) - } - } - - override fun getLatestNetworkInfo(): NetworkInfo { - return networkInfo - } - - // endregion - - // region Internal - - private fun getStrength(networkCapabilities: NetworkCapabilities): Int { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - networkCapabilities.signalStrength - } else { - Int.MIN_VALUE - } - } - - private fun getNetworkType(networkCapabilities: NetworkCapabilities): NetworkInfo.Connectivity { - return if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { - NetworkInfo.Connectivity.NETWORK_WIFI - } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { - NetworkInfo.Connectivity.NETWORK_ETHERNET - } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - NetworkInfo.Connectivity.NETWORK_CELLULAR - } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) { - NetworkInfo.Connectivity.NETWORK_BLUETOOTH - } else { - NetworkInfo.Connectivity.NETWORK_OTHER - } - } - - // endregion - - companion object { - internal const val ERROR_REGISTER = "We couldn't register a Network Callback, " + - "the network information reported will be less accurate." - internal const val ERROR_UNREGISTER = "We couldn't unregister the Network Callback" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfo.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfo.kt deleted file mode 100644 index 5d986bb91b..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/info/NetworkInfo.kt +++ /dev/null @@ -1,37 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net.info - -internal data class NetworkInfo( - val connectivity: Connectivity = Connectivity.NETWORK_NOT_CONNECTED, - val carrierName: String? = null, - val carrierId: Int = -1, - val upKbps: Int = -1, - val downKbps: Int = -1, - val strength: Int = Int.MIN_VALUE, - val cellularTechnology: String? = null -) { - - fun isConnected(): Boolean { - return connectivity != Connectivity.NETWORK_NOT_CONNECTED - } - - internal enum class Connectivity(val serialized: String) { - NETWORK_NOT_CONNECTED("network_not_connected"), - NETWORK_ETHERNET("network_ethernet"), - NETWORK_WIFI("network_wifi"), - NETWORK_WIMAX("network_wimax"), - NETWORK_BLUETOOTH("network_bluetooth"), - NETWORK_2G("network_2g"), - NETWORK_3G("network_3g"), - NETWORK_4G("network_4g"), - NETWORK_5G("network_5g"), - NETWORK_MOBILE_OTHER("network_mobile_other"), - NETWORK_CELLULAR("network_cellular"), - NETWORK_OTHER("network_other") - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiver.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiver.kt deleted file mode 100644 index 65a5ba70a6..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/receiver/ThreadSafeReceiver.kt +++ /dev/null @@ -1,34 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import java.util.concurrent.atomic.AtomicBoolean - -internal abstract class ThreadSafeReceiver : BroadcastReceiver() { - - val isRegistered = AtomicBoolean(false) - - fun registerReceiver( - context: Context, - filter: IntentFilter - ): Intent? { - - val intent = context.registerReceiver(this, filter) - isRegistered.set(true) - return intent - } - - fun unregisterReceiver(context: Context) { - if (isRegistered.compareAndSet(true, false)) { - context.unregisterReceiver(this) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/sampling/RateBasedSampler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/sampling/RateBasedSampler.kt deleted file mode 100644 index 2978ed0251..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/sampling/RateBasedSampler.kt +++ /dev/null @@ -1,23 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.sampling - -import java.security.SecureRandom - -internal class RateBasedSampler(internal val sampleRate: Float) : Sampler { - private val random by lazy { SecureRandom() } - - override fun sample(): Boolean { - if (sampleRate == 0f) { - return false - } - if (sampleRate == 1f) { - return true - } - return random.nextFloat() <= sampleRate - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/sampling/Sampler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/sampling/Sampler.kt deleted file mode 100644 index 0c8072038d..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/sampling/Sampler.kt +++ /dev/null @@ -1,16 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.sampling - -internal interface Sampler { - - /** - * Sampling method. - * @return true if you want to keep the value, false otherwise. - */ - fun sample(): Boolean -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProvider.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProvider.kt deleted file mode 100644 index 50b6f64f6a..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProvider.kt +++ /dev/null @@ -1,94 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.system - -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.BatteryManager -import android.os.Build -import android.os.PowerManager -import com.datadog.android.core.internal.receiver.ThreadSafeReceiver -import com.datadog.android.core.internal.utils.sdkLogger - -internal class BroadcastReceiverSystemInfoProvider : - ThreadSafeReceiver(), SystemInfoProvider { - - private var systemInfo: SystemInfo = SystemInfo() - - // region BroadcastReceiver - - override fun onReceive(context: Context, intent: Intent?) { - val action = intent?.action - when (action) { - Intent.ACTION_BATTERY_CHANGED -> { - sdkLogger.d("received battery update") - handleBatteryIntent(intent) - } - PowerManager.ACTION_POWER_SAVE_MODE_CHANGED -> { - sdkLogger.d("received power save mode update") - handlePowerSaveIntent(context) - } - else -> sdkLogger.d("received unknown update $action") - } - } - - // endregion - - // region SystemInfoProvider - - override fun register(context: Context) { - registerIntentFilter(context, Intent.ACTION_BATTERY_CHANGED) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - registerIntentFilter(context, PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) - } - } - - override fun unregister(context: Context) { - unregisterReceiver(context) - } - - override fun getLatestSystemInfo(): SystemInfo { - return systemInfo - } - - // endregion - - // region Internal - - private fun registerIntentFilter(context: Context, action: String) { - val filter = IntentFilter() - filter.addAction(action) - registerReceiver(context, filter)?.let { onReceive(context, it) } - } - - private fun handleBatteryIntent(intent: Intent) { - val status = intent.getIntExtra( - BatteryManager.EXTRA_STATUS, - BatteryManager.BATTERY_STATUS_UNKNOWN - ) - val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100) - - systemInfo = systemInfo.copy( - batteryStatus = SystemInfo.BatteryStatus.fromAndroidStatus(status), - batteryLevel = (level * 100) / scale - ) - } - - private fun handlePowerSaveIntent(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager - val powerSaveMode = powerManager?.isPowerSaveMode ?: false - systemInfo = systemInfo.copy( - powerSaveMode = powerSaveMode - ) - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/threading/AndroidDeferredHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/threading/AndroidDeferredHandler.kt deleted file mode 100644 index 59d7a6132a..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/threading/AndroidDeferredHandler.kt +++ /dev/null @@ -1,18 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.threading - -import android.os.Handler - -internal open class AndroidDeferredHandler( - private val handler: Handler -) : DeferredHandler { - - override fun handle(r: Runnable) { - handler.post(r) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/threading/DeferredHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/threading/DeferredHandler.kt deleted file mode 100644 index 2909a06a48..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/threading/DeferredHandler.kt +++ /dev/null @@ -1,11 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.threading - -internal interface DeferredHandler { - fun handle(r: Runnable) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/LoggingSyncListener.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/LoggingSyncListener.kt deleted file mode 100644 index 5d4a4c1d73..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/LoggingSyncListener.kt +++ /dev/null @@ -1,24 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.time - -import com.datadog.android.core.internal.utils.sdkLogger -import com.lyft.kronos.SyncListener - -internal class LoggingSyncListener : SyncListener { - override fun onStartSync(host: String) { - sdkLogger.d("onStartSync @host:$host") - } - - override fun onSuccess(ticksDelta: Long, responseTimeMs: Long) { - sdkLogger.d("onSuccess @ticksDelta:$ticksDelta @responseTimeMs:$responseTimeMs") - } - - override fun onError(host: String, throwable: Throwable) { - sdkLogger.e("onError @host:host", throwable) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/TimeProvider.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/TimeProvider.kt deleted file mode 100644 index 96ba1a9909..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/time/TimeProvider.kt +++ /dev/null @@ -1,19 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.time - -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface TimeProvider { - - fun getDeviceTimestamp(): Long - - fun getServerTimestamp(): Long - - fun getServerOffsetNanos(): Long -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ByteArrayExt.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ByteArrayExt.kt deleted file mode 100644 index d4abe2fc7a..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ByteArrayExt.kt +++ /dev/null @@ -1,47 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -/** - * Splits this [ByteArray] to a list of [ByteArray] around occurrences of the specified [delimiter]. - * - * @param delimiter a byte to be used as delimiter. -*/ -internal fun ByteArray.split(delimiter: Byte): List { - val result = mutableListOf() - - var offset = 0 - var nextIndex: Int - - do { - nextIndex = indexOf(delimiter, offset) - val length = if (nextIndex >= 0) nextIndex - offset else size - offset - if (length > 0) { - val subArray = ByteArray(length) - System.arraycopy(this, offset, subArray, 0, length) - result.add(subArray) - } - offset = nextIndex + 1 - } while (nextIndex != -1) - - return result -} - -/** - * Returns the index within this [ByteArray] of the first occurrence of the specified [b], - * starting from the specified [startIndex]. - * - * @return An index of the first occurrence of [b] or `-1` if none is found. - */ -internal fun ByteArray.indexOf(b: Byte, startIndex: Int = 0): Int { - for (i in startIndex until size) { - if (get(i) == b) { - return i - } - } - return -1 -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ComponentPredicateExt.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ComponentPredicateExt.kt deleted file mode 100644 index dadb50c005..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ComponentPredicateExt.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import com.datadog.android.rum.tracking.ComponentPredicate - -/** - * Executes the provided operation if the predicate verifies the argument. - * @param arg to be verified - * @param operation to be executed - */ -internal inline fun ComponentPredicate.runIfValid(arg: T, operation: (T) -> Unit) { - if (accept(arg)) { - operation(arg) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/MapUtils.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/MapUtils.kt deleted file mode 100644 index ab8d6cedd6..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/MapUtils.kt +++ /dev/null @@ -1,9 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -internal val NULL_MAP_VALUE = Object() diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt deleted file mode 100644 index a2aa00dda9..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/MiscUtils.kt +++ /dev/null @@ -1,59 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import com.google.gson.JsonArray -import com.google.gson.JsonElement -import com.google.gson.JsonNull -import com.google.gson.JsonObject -import com.google.gson.JsonPrimitive -import java.util.Date - -internal inline fun retryWithDelay( - block: () -> Boolean, - times: Int, - loopsDelayInNanos: Long -): Boolean { - var retryCounter = 1 - var wasSuccessful = false - var loopTimeOrigin = System.nanoTime() - loopsDelayInNanos - while (retryCounter <= times && !wasSuccessful) { - if ((System.nanoTime() - loopTimeOrigin) >= loopsDelayInNanos) { - wasSuccessful = block() - loopTimeOrigin = System.nanoTime() - retryCounter++ - } - } - return wasSuccessful -} - -internal fun Any?.toJsonElement(): JsonElement { - return when (this) { - NULL_MAP_VALUE -> JsonNull.INSTANCE - null -> JsonNull.INSTANCE - is Boolean -> JsonPrimitive(this) - is Int -> JsonPrimitive(this) - is Long -> JsonPrimitive(this) - is Float -> JsonPrimitive(this) - is Double -> JsonPrimitive(this) - is String -> JsonPrimitive(this) - is Date -> JsonPrimitive(this.time) - is Iterable<*> -> this.toJsonArray() - is JsonObject -> this - is JsonArray -> this - is JsonPrimitive -> this - else -> JsonPrimitive(toString()) - } -} - -internal fun Iterable<*>.toJsonArray(): JsonElement { - val array = JsonArray() - forEach { - array.add(it.toJsonElement()) - } - return array -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/RuntimeUtils.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/RuntimeUtils.kt deleted file mode 100644 index 47d934cda2..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/RuntimeUtils.kt +++ /dev/null @@ -1,50 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import com.datadog.android.BuildConfig -import com.datadog.android.Datadog -import com.datadog.android.log.Logger -import com.datadog.android.log.internal.logger.ConditionalLogHandler -import com.datadog.android.log.internal.logger.LogcatLogHandler -import com.datadog.android.log.internal.logger.NoOpLogHandler - -internal const val SDK_LOG_PREFIX = "DD_LOG" -internal const val DEV_LOG_PREFIX = "Datadog" - -/** - * Global SDK Logger. This logger is meant for internal debugging purposes. Should not post logs - * to Datadog endpoint and should be conditioned by the BuildConfig flag. - */ -internal val sdkLogger: Logger = buildSdkLogger() - -internal fun buildSdkLogger(): Logger { - val handler = if (BuildConfig.LOGCAT_ENABLED) { - LogcatLogHandler(SDK_LOG_PREFIX, true) - } else { - NoOpLogHandler() - } - return Logger(handler) -} - -/** - * Global Dev Logger. This logger is meant for user's debugging purposes. Should not post logs - * to Datadog endpoint and should be conditioned by the Datadog Verbosity level. - */ -internal val devLogger: Logger = buildDevLogger() - -private fun buildDevLogger(): Logger { - return Logger(buildDevLogHandler()) -} - -internal fun buildDevLogHandler(): ConditionalLogHandler { - return ConditionalLogHandler( - LogcatLogHandler(DEV_LOG_PREFIX, false) - ) { i, _ -> - i >= Datadog.libraryVerbosity - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ThrowableExt.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ThrowableExt.kt deleted file mode 100644 index f78cd36b0f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ThrowableExt.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.datadog.android.core.internal.utils - -import java.io.PrintWriter -import java.io.StringWriter - -internal fun Throwable.loggableStackTrace(): String { - val stringWriter = StringWriter() - printStackTrace(PrintWriter(stringWriter)) - return stringWriter.toString() -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ViewUtils.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ViewUtils.kt deleted file mode 100644 index b3749a312e..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/ViewUtils.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.datadog.android.core.internal.utils - -internal fun Any.resolveViewName(): String { - return javaClass.canonicalName ?: javaClass.simpleName -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtils.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtils.kt deleted file mode 100644 index 723c51acf4..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtils.kt +++ /dev/null @@ -1,57 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import android.content.Context -import androidx.work.Constraints -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import com.datadog.android.core.internal.data.upload.UploadWorker -import java.lang.IllegalStateException -import java.util.concurrent.TimeUnit - -internal const val CANCEL_ERROR_MESSAGE = "Error cancelling the UploadWorker" -internal const val SETUP_ERROR_MESSAGE = "Error while trying to setup the upload worker." -internal const val UPLOAD_WORKER_NAME = "DatadogUploadWorker" -internal const val TAG_DATADOG_UPLOAD = "DatadogBackgroundUpload" - -internal const val DELAY_MS: Long = 5000 - -internal fun cancelUploadWorker(context: Context) { - sdkLogger.i("Cancelling UploadWorker") - try { - val workManager = WorkManager.getInstance(context) - workManager.cancelAllWorkByTag(TAG_DATADOG_UPLOAD) - } catch (e: IllegalStateException) { - sdkLogger.e(CANCEL_ERROR_MESSAGE, e) - } -} - -internal fun triggerUploadWorker(context: Context) { - try { - sdkLogger.i("Triggering UploadWorker") - val workManager = WorkManager.getInstance(context) - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - val uploadWorkRequest = OneTimeWorkRequest.Builder(UploadWorker::class.java) - .setConstraints(constraints) - .addTag(TAG_DATADOG_UPLOAD) - .setInitialDelay(DELAY_MS, TimeUnit.MILLISECONDS) - .build() - workManager - .enqueueUniqueWork( - UPLOAD_WORKER_NAME, - ExistingWorkPolicy.REPLACE, - uploadWorkRequest - ) - } catch (e: IllegalStateException) { - sdkLogger.e(SETUP_ERROR_MESSAGE, e) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/CrashLogFileStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/CrashLogFileStrategy.kt deleted file mode 100644 index 51537bfff2..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/CrashLogFileStrategy.kt +++ /dev/null @@ -1,47 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.error.internal - -import android.content.Context -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.FilePersistenceStrategy -import com.datadog.android.core.internal.domain.PayloadDecoration -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.domain.LogSerializer -import java.io.File -import java.util.concurrent.ExecutorService - -internal class CrashLogFileStrategy( - context: Context, - filePersistenceConfig: FilePersistenceConfig = - FilePersistenceConfig(recentDelayMs = MAX_DELAY_BETWEEN_LOGS_MS), - dataPersistenceExecutorService: ExecutorService, - trackingConsentProvider: ConsentProvider -) : FilePersistenceStrategy( - File(context.filesDir, INTERMEDIATE_DATA_FOLDER), - File(context.filesDir, AUTHORIZED_FOLDER), - LogSerializer(), - dataPersistenceExecutorService, - filePersistenceConfig, - PayloadDecoration.JSON_ARRAY_DECORATION, - trackingConsentProvider -) { - companion object { - internal const val VERSION = 1 - internal const val ROOT = "dd-crash" - internal const val INTERMEDIATE_DATA_FOLDER = - "$ROOT-pending-v$VERSION" - internal const val AUTHORIZED_FOLDER = "$ROOT-v$VERSION" - internal const val MAX_DELAY_BETWEEN_LOGS_MS = 5000L - } - - override fun getWriter(): Writer { - return super.consentAwareDataWriter.getInternalWriter() - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/CrashReportsFeature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/CrashReportsFeature.kt deleted file mode 100644 index e4e7bd9abb..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/CrashReportsFeature.kt +++ /dev/null @@ -1,157 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.error.internal - -import android.content.Context -import com.datadog.android.DatadogConfig -import com.datadog.android.DatadogEndpoint -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.SdkFeature -import com.datadog.android.core.internal.data.upload.DataUploadScheduler -import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler -import com.datadog.android.core.internal.data.upload.UploadScheduler -import com.datadog.android.core.internal.domain.NoOpPersistenceStrategy -import com.datadog.android.core.internal.domain.PersistenceStrategy -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.NoOpDataUploader -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.domain.LogGenerator -import com.datadog.android.log.internal.net.LogsOkHttpUploader -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.plugin.DatadogPluginConfig -import java.util.concurrent.ExecutorService -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.atomic.AtomicBoolean -import okhttp3.OkHttpClient - -internal object CrashReportsFeature : SdkFeature() { - - internal var originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() - internal val initialized = AtomicBoolean(false) - - internal var clientToken: String = "" - internal var endpointUrl: String = DatadogEndpoint.LOGS_US - internal var persistenceStrategy: PersistenceStrategy = NoOpPersistenceStrategy() - internal var uploader: DataUploader = NoOpDataUploader() - internal var dataUploadScheduler: UploadScheduler = NoOpUploadScheduler() - - @Suppress("LongParameterList") - fun initialize( - appContext: Context, - config: DatadogConfig.FeatureConfig, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - userInfoProvider: UserInfoProvider, - systemInfoProvider: SystemInfoProvider, - dataUploadThreadPoolExecutor: ScheduledThreadPoolExecutor, - dataPersistenceExecutor: ExecutorService, - trackingConsentProvider: ConsentProvider - ) { - - if (initialized.get()) { - return - } - - clientToken = config.clientToken - endpointUrl = config.endpointUrl - - persistenceStrategy = CrashLogFileStrategy( - appContext, - dataPersistenceExecutorService = dataPersistenceExecutor, - trackingConsentProvider = trackingConsentProvider - ) - setupUploader( - endpointUrl, - okHttpClient, - networkInfoProvider, - systemInfoProvider, - dataUploadThreadPoolExecutor - ) - registerPlugins( - config.plugins, - DatadogPluginConfig.CrashReportsPluginConfig( - appContext, - config.envName, - CoreFeature.serviceName, - trackingConsentProvider.getConsent() - ), - trackingConsentProvider - ) - setupExceptionHandler(appContext, networkInfoProvider, userInfoProvider) - - initialized.set(true) - } - - fun clearAllData() { - persistenceStrategy.clearAllData() - } - - fun stop() { - if (initialized.get()) { - unregisterPlugins() - Thread.setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler) - dataUploadScheduler.stopScheduling() - - persistenceStrategy = NoOpPersistenceStrategy() - uploader = NoOpDataUploader() - dataUploadScheduler = NoOpUploadScheduler() - clientToken = "" - endpointUrl = DatadogEndpoint.LOGS_US - - initialized.set(false) - } - } - - // region Internal - - private fun setupUploader( - endpointUrl: String, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - systemInfoProvider: SystemInfoProvider, - scheduledThreadPoolExecutor: ScheduledThreadPoolExecutor - ) { - dataUploadScheduler = if (CoreFeature.isMainProcess) { - uploader = LogsOkHttpUploader(endpointUrl, clientToken, okHttpClient) - DataUploadScheduler( - persistenceStrategy.getReader(), - uploader, - networkInfoProvider, - systemInfoProvider, - scheduledThreadPoolExecutor - ) - } else { - NoOpUploadScheduler() - } - dataUploadScheduler.startScheduling() - } - - private fun setupExceptionHandler( - appContext: Context, - networkInfoProvider: NetworkInfoProvider, - userInfoProvider: UserInfoProvider - ) { - originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() - DatadogExceptionHandler( - LogGenerator( - CoreFeature.serviceName, - DatadogExceptionHandler.LOGGER_NAME, - networkInfoProvider, - userInfoProvider, - CoreFeature.envName, - CoreFeature.packageVersion - ), - writer = persistenceStrategy.getWriter(), - appContext = appContext - ).register() - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/DatadogExceptionHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/DatadogExceptionHandler.kt deleted file mode 100644 index 7ba5f0404f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/error/internal/DatadogExceptionHandler.kt +++ /dev/null @@ -1,80 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.error.internal - -import android.content.Context -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.utils.triggerUploadWorker -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.domain.LogGenerator -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import java.lang.ref.WeakReference - -internal class DatadogExceptionHandler( - private val logGenerator: LogGenerator, - private val writer: Writer, - appContext: Context? -) : - Thread.UncaughtExceptionHandler { - - private val contextRef = WeakReference(appContext) - private var previousHandler: Thread.UncaughtExceptionHandler? = null - - // region Thread.UncaughtExceptionHandler - - override fun uncaughtException(t: Thread, e: Throwable) { - // write the log immediately - writer.write(createLog(t, e)) - - // write a RUM Error too - (GlobalRum.get() as? AdvancedRumMonitor)?.addCrash(MESSAGE, RumErrorSource.SOURCE, e) - - // trigger a task to send the logs ASAP - contextRef.get()?.let { - triggerUploadWorker(it) - } - - // Always do this one last; this will shut down the VM - previousHandler?.uncaughtException(t, e) - } - - // endregion - - // region DatadogExceptionHandler - - fun register() { - previousHandler = Thread.getDefaultUncaughtExceptionHandler() - Thread.setDefaultUncaughtExceptionHandler(this) - } - - // endregion - - // region Internal - - private fun createLog(thread: Thread, throwable: Throwable): Log { - return logGenerator.generateLog( - Log.CRASH, - MESSAGE, - throwable, - emptyMap(), - emptySet(), - System.currentTimeMillis(), - thread.name - ) - } - - // endregion - - companion object { - // If you change these you will have to propagate the changes - // also into the datadog-native-lib.cpp file inside the dd-sdk-android-ndk module. - internal const val LOGGER_NAME = "crash" - internal const val MESSAGE = "Application crash detected" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/EndpointUpdateStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/EndpointUpdateStrategy.kt deleted file mode 100644 index 7408c7ebf9..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/EndpointUpdateStrategy.kt +++ /dev/null @@ -1,24 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log - -/** - * The strategy on how to deal with older data when the - * endpoint needs to be changed. - */ -@Deprecated("This was only meant as an internal feature and is not needed anymore.") -enum class EndpointUpdateStrategy { - - /** - * All previous unsent data will be deleted and lost forever. - */ - DISCARD_OLD_DATA, - /** - * All previous unsent data will be sent to the new endpoint. - */ - SEND_OLD_DATA_TO_NEW_ENDPOINT -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/Logger.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/Logger.kt deleted file mode 100644 index 422f3aefef..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/Logger.kt +++ /dev/null @@ -1,540 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log - -import android.util.Log as AndroidLog -import androidx.annotation.FloatRange -import com.datadog.android.Datadog -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.sampling.RateBasedSampler -import com.datadog.android.core.internal.utils.NULL_MAP_VALUE -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.log.internal.LogsFeature -import com.datadog.android.log.internal.domain.LogGenerator -import com.datadog.android.log.internal.logger.CombinedLogHandler -import com.datadog.android.log.internal.logger.DatadogLogHandler -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.log.internal.logger.LogcatLogHandler -import com.datadog.android.log.internal.logger.NoOpLogHandler -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import java.util.Date -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArraySet - -/** - * A class enabling Datadog logging features. - * - * It allows you to create a specific context (automatic information, custom attributes, tags) that will be embedded in all - * logs sent through this logger. - * - * You can have multiple loggers configured in your application, each with their own settings. - */ -@Suppress("TooManyFunctions", "MethodOverloading") -class Logger -internal constructor(private val handler: LogHandler) { - - private val attributes = ConcurrentHashMap() - private val tags = CopyOnWriteArraySet() - - // region Log - - /** - * Sends a VERBOSE log message. - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exist in this logger, it will be overridden (just for this message) - */ - @Suppress("FunctionMinLength") - @JvmOverloads - fun v( - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap() - ) { - internalLog(AndroidLog.VERBOSE, message, throwable, attributes) - } - - /** - * Sends a Debug log message. - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exist in this logger, it will be overridden (just for this message) - */ - @Suppress("FunctionMinLength") - @JvmOverloads - fun d( - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap() - ) { - internalLog(AndroidLog.DEBUG, message, throwable, attributes) - } - - /** - * Sends an Info log message. - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exist in this logger, it will be overridden (just for this message) - */ - @Suppress("FunctionMinLength") - @JvmOverloads - fun i( - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap() - ) { - internalLog(AndroidLog.INFO, message, throwable, attributes) - } - - /** - * Sends a Warning log message. - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exist in this logger, it will be overridden (just for this message) - */ - @Suppress("FunctionMinLength") - @JvmOverloads - fun w( - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap() - ) { - internalLog(AndroidLog.WARN, message, throwable, attributes) - } - - /** - * Sends an Error log message. - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exist in this logger, it will be overridden (just for this message) - */ - @Suppress("FunctionMinLength") - @JvmOverloads - fun e( - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap() - ) { - internalLog(AndroidLog.ERROR, message, throwable, attributes) - } - - /** - * Sends an Assert log message. - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exist in this logger, it will be overridden (just for this message) - */ - @Suppress("FunctionMinLength") - @JvmOverloads - fun wtf( - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap() - ) { - internalLog(AndroidLog.ASSERT, message, throwable, attributes) - } - - /** - * Sends a log message. - * - * @param priority the priority level (must be one of the Android Log.* constants) - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exist in this logger, it will be overridden (just for this message) - * - */ - @JvmOverloads - fun log( - priority: Int, - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap() - ) { - internalLog(priority, message, throwable, attributes) - } - - // endregion - - // region Builder - - /** - * A Builder class for a [Logger]. - */ - class Builder { - - private var serviceName: String = CoreFeature.serviceName - private var datadogLogsEnabled: Boolean = true - private var logcatLogsEnabled: Boolean = false - private var networkInfoEnabled: Boolean = false - private var bundleWithTraceEnabled: Boolean = true - private var bundleWithRumEnabled: Boolean = true - private var loggerName: String = CoreFeature.packageName - private var sampleRate: Float = 1.0f - - /** - * Builds a [Logger] based on the current state of this Builder. - */ - fun build(): Logger { - - val handler = when { - datadogLogsEnabled && logcatLogsEnabled -> { - CombinedLogHandler( - buildDatadogHandler(), - buildLogcatHandler() - ) - } - datadogLogsEnabled -> buildDatadogHandler() - logcatLogsEnabled -> buildLogcatHandler() - else -> NoOpLogHandler() - } - - return Logger(handler) - } - - /** - * Sets the service name that will appear in your logs. - * @param serviceName the service name (default = "android") - */ - fun setServiceName(serviceName: String): Builder { - this.serviceName = serviceName - return this - } - - /** - * Enables your logs to be sent to the Datadog servers. - * You can use this feature to disable Datadog logs based on a configuration or an application flavor. - * @param enabled true by default - */ - fun setDatadogLogsEnabled(enabled: Boolean): Builder { - datadogLogsEnabled = enabled - return this - } - - /** - * Enables your logs to be duplicated in LogCat. - * @param enabled false by default - */ - fun setLogcatLogsEnabled(enabled: Boolean): Builder { - logcatLogsEnabled = enabled - return this - } - - /** - * Enables network information to be automatically added in your logs. - * @param enabled false by default - */ - fun setNetworkInfoEnabled(enabled: Boolean): Builder { - networkInfoEnabled = enabled - return this - } - - /** - * Sets the logger name that will appear in your logs when a throwable is attached. - * @param name the logger custom name (default = application package name) - */ - fun setLoggerName(name: String): Builder { - loggerName = name - return this - } - - /** - * Enables the logs bundling with the current active trace. If this feature is enabled all - * the logs from this moment on will be bundled with the current trace and you will be able - * to see all the logs sent during a specific trace. - * @param enabled true by default - */ - fun setBundleWithTraceEnabled(enabled: Boolean): Builder { - bundleWithTraceEnabled = enabled - return this - } - - /** - * Enables the logs bundling with the current active View. If this feature is enabled all - * the logs from this moment on will be bundled with the current view information and you - * will be able to see all the logs sent during a specific view in the Rum Explorer. - * @param enabled true by default - */ - fun setBundleWithRumEnabled(enabled: Boolean): Builder { - bundleWithRumEnabled = enabled - return this - } - - /** - * Sets the sample rate for this Logger. - * @param rate the sampling rate, in percent. - * A value of `0.3` means we'll send 30% of the logs. - * Default is 1.0 (ie: all logs are sent). - */ - fun setSampleRate(@FloatRange(from = 0.0, to = 1.0) rate: Float): Builder { - sampleRate = rate - return this - } - - // region Internal - - private fun buildLogcatHandler(): LogHandler { - return LogcatLogHandler(serviceName, true) - } - - private fun buildDatadogHandler(): LogHandler { - return if (!LogsFeature.isInitialized()) { - devLogger.e(Datadog.MESSAGE_NOT_INITIALIZED) - NoOpLogHandler() - } else { - val netInfoProvider = if (networkInfoEnabled) { - CoreFeature.networkInfoProvider - } else { - null - } - DatadogLogHandler( - logGenerator = LogGenerator( - serviceName, - loggerName, - netInfoProvider, - CoreFeature.userInfoProvider, - CoreFeature.envName, - CoreFeature.packageVersion - ), - writer = LogsFeature.persistenceStrategy.getWriter(), - bundleWithTraces = bundleWithTraceEnabled, - bundleWithRum = bundleWithRumEnabled, - sampler = RateBasedSampler(sampleRate) - ) - } - } - - // endregion - } - - // endregion - - // region Context Information (attributes, tags) - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the boolean value of this attribute - */ - fun addAttribute(key: String, value: Boolean) { - attributes[key] = value - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the integer value of this attribute - */ - fun addAttribute(key: String, value: Int) { - attributes[key] = value - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the long value of this attribute - */ - fun addAttribute(key: String, value: Long) { - attributes[key] = value - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the float value of this attribute - */ - fun addAttribute(key: String, value: Float) { - attributes[key] = value - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the double value of this attribute - */ - fun addAttribute(key: String, value: Double) { - attributes[key] = value - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the (nullable) String value of this attribute - */ - fun addAttribute(key: String, value: String?) { - safelyAddAttribute(key, value) - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * @param key the key for this attribute - * @param value the (nullable) [Date] value of this attribute - */ - fun addAttribute(key: String, value: Date?) { - safelyAddAttribute(key, value) - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the (nullable) [JsonObject] value of this attribute - */ - fun addAttribute(key: String, value: JsonObject?) { - safelyAddAttribute(key, value) - } - - /** - * Add a custom attribute to all future logs sent by this logger. - * - * Values can be nested up to 10 levels deep. Keys - * using more than 10 levels will be sanitized by SDK. - * - * @param key the key for this attribute - * @param value the (nullable) [JsonArray] value of this attribute - */ - fun addAttribute(key: String, value: JsonArray?) { - safelyAddAttribute(key, value) - } - - /** - * Remove a custom attribute from all future logs sent by this logger. - * Previous logs won't lose the attribute value associated with this key if they were created - * prior to this call. - * @param key the key of the attribute to remove - */ - fun removeAttribute(key: String) { - attributes.remove(key) - } - - /** - * Add a tag to all future logs sent by this logger. - * The tag will take the form "key:value". - * - * Tags must start with a letter and after that may contain the following characters: - * Alphanumerics, Underscores, Minuses, Colons, Periods, Slashes. Other special characters - * are converted to underscores. - * Tags must be lowercase, and can be at most 200 characters. If the tag you provide is - * longer, only the first 200 characters will be used. - * - * @param key the key for this tag - * @param value the (non null) value of this tag - * @see [documentation](https://docs.datadoghq.com/tagging/#defining-tags) - */ - fun addTag(key: String, value: String) { - addTagInternal("$key:$value") - } - - /** - * Add a tag to all future logs sent by this logger. - * - * Tags must start with a letter and after that may contain the following characters: - * Alphanumerics, Underscores, Minuses, Colons, Periods, Slashes. Other special characters - * are converted to underscores. - * Tags must be lowercase, and can be at most 200 characters. If the tag you provide is - * longer, only the first 200 characters will be used. - * - * @param tag the (non null) tag - * @see [documentation](https://docs.datadoghq.com/tagging/#defining-tags) - */ - fun addTag(tag: String) { - addTagInternal(tag) - } - - /** - * Remove a tag from all future logs sent by this logger. - * Previous logs won't lose the this tag if they were created prior to this call. - * @param tag the tag to remove - */ - fun removeTag(tag: String) { - removeTagInternal(tag) - } - - /** - * Remove all tags with the given key from all future logs sent by this logger. - * Previous logs won't lose the this tag if they were created prior to this call. - * @param key the key of the tags to remove - */ - fun removeTagsWithKey(key: String) { - val prefix = "$key:" - safelyRemoveTagsWithKey { - it.startsWith(prefix) - } - } - - // endregion - - // region Internal - - internal fun internalLog( - level: Int, - message: String, - throwable: Throwable?, - localAttributes: Map, - timestamp: Long? = null - ) { - val combinedAttributes = mutableMapOf() - combinedAttributes.putAll(attributes) - combinedAttributes.putAll(localAttributes) - handler.handleLog(level, message, throwable, combinedAttributes, tags, timestamp) - } - - private fun addTagInternal(tag: String) { - tags.add(tag) - } - - private fun removeTagInternal(tag: String) { - tags.remove(tag) - } - - private fun safelyAddAttribute(key: String, value: Any?) { - val attributeValue = value ?: NULL_MAP_VALUE - attributes[key] = attributeValue - } - - private fun safelyRemoveTagsWithKey(keyFilter: (String) -> Boolean) { - // we first gather all the objects we want to remove based on a copy - val toRemove = tags.toTypedArray().filter(keyFilter) - tags.removeAll(toRemove) - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/LogsFeature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/LogsFeature.kt deleted file mode 100644 index 53631e8c5f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/LogsFeature.kt +++ /dev/null @@ -1,132 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal - -import android.content.Context -import com.datadog.android.DatadogConfig -import com.datadog.android.DatadogEndpoint -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.SdkFeature -import com.datadog.android.core.internal.data.upload.DataUploadScheduler -import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler -import com.datadog.android.core.internal.data.upload.UploadScheduler -import com.datadog.android.core.internal.domain.NoOpPersistenceStrategy -import com.datadog.android.core.internal.domain.PersistenceStrategy -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.NoOpDataUploader -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.domain.LogFileStrategy -import com.datadog.android.log.internal.net.LogsOkHttpUploader -import com.datadog.android.plugin.DatadogPluginConfig -import java.util.concurrent.ExecutorService -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.atomic.AtomicBoolean -import okhttp3.OkHttpClient - -internal object LogsFeature : SdkFeature() { - - internal val initialized = AtomicBoolean(false) - - internal var clientToken: String = "" - internal var endpointUrl: String = DatadogEndpoint.LOGS_US - internal var persistenceStrategy: PersistenceStrategy = NoOpPersistenceStrategy() - internal var uploader: DataUploader = NoOpDataUploader() - internal var dataUploadScheduler: UploadScheduler = NoOpUploadScheduler() - - @Suppress("LongParameterList") - fun initialize( - appContext: Context, - config: DatadogConfig.FeatureConfig, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - systemInfoProvider: SystemInfoProvider, - dataUploadThreadPoolExecutor: ScheduledThreadPoolExecutor, - dataPersistenceExecutor: ExecutorService, - trackingConsentProvider: ConsentProvider - ) { - if (initialized.get()) { - return - } - - clientToken = config.clientToken - endpointUrl = config.endpointUrl - persistenceStrategy = LogFileStrategy( - appContext, - trackingConsentProvider = trackingConsentProvider, - dataPersistenceExecutorService = dataPersistenceExecutor - ) - setupUploader( - endpointUrl, - okHttpClient, - networkInfoProvider, - systemInfoProvider, - dataUploadThreadPoolExecutor - ) - - registerPlugins( - config.plugins, - DatadogPluginConfig.LogsPluginConfig( - appContext, - config.envName, - CoreFeature.serviceName, - trackingConsentProvider.getConsent() - ), - trackingConsentProvider - ) - initialized.set(true) - } - - fun isInitialized(): Boolean { - return initialized.get() - } - - fun clearAllData() { - persistenceStrategy.clearAllData() - } - - fun stop() { - if (initialized.get()) { - unregisterPlugins() - dataUploadScheduler.stopScheduling() - persistenceStrategy = NoOpPersistenceStrategy() - dataUploadScheduler = NoOpUploadScheduler() - clientToken = "" - endpointUrl = DatadogEndpoint.LOGS_US - - initialized.set(false) - } - } - - // region Internal - - private fun setupUploader( - endpointUrl: String, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - systemInfoProvider: SystemInfoProvider, - dataUploadThreadPoolExecutor: ScheduledThreadPoolExecutor - ) { - dataUploadScheduler = if (CoreFeature.isMainProcess) { - uploader = LogsOkHttpUploader(endpointUrl, clientToken, okHttpClient) - DataUploadScheduler( - persistenceStrategy.getReader(), - uploader, - networkInfoProvider, - systemInfoProvider, - dataUploadThreadPoolExecutor - ) - } else { - NoOpUploadScheduler() - } - dataUploadScheduler.startScheduling() - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/Log.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/Log.kt deleted file mode 100644 index c23e2ec24b..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/Log.kt +++ /dev/null @@ -1,32 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.log.internal.user.UserInfo - -/** - * Represents a Log before it is persisted and sent to Datadog servers. - */ -internal data class Log( - val serviceName: String, - val level: Int, - val message: String, - val timestamp: Long, - val attributes: Map, - val tags: List, - val throwable: Throwable?, - val networkInfo: NetworkInfo?, - val userInfo: UserInfo, - val loggerName: String, - val threadName: String -) { - - companion object { - internal const val CRASH: Int = 9 - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogFileDataMigrator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogFileDataMigrator.kt deleted file mode 100644 index 66aeb8e951..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogFileDataMigrator.kt +++ /dev/null @@ -1,35 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import com.datadog.android.core.internal.data.DataMigrator -import java.io.File - -internal class LogFileDataMigrator( - private val rootDirectory: File -) : DataMigrator { - - private val patches: MutableMap Unit> = HashMap() - - init { - patches[LogFileStrategy.INTERMEDIATE_DATA_FOLDER] = { - if (it.exists()) { - it.deleteRecursively() - } - } - } - - override fun migrateData() { - rootDirectory.listFiles() - ?.let { - it.filter { patches.containsKey(it.name) } - .forEach { file -> - patches[file.name]?.invoke(file) - } - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogFileStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogFileStrategy.kt deleted file mode 100644 index 73561e3926..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogFileStrategy.kt +++ /dev/null @@ -1,40 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import android.content.Context -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.FilePersistenceStrategy -import com.datadog.android.core.internal.domain.PayloadDecoration -import com.datadog.android.core.internal.privacy.ConsentProvider -import java.io.File -import java.util.concurrent.ExecutorService - -internal class LogFileStrategy( - context: Context, - filePersistenceConfig: FilePersistenceConfig = - FilePersistenceConfig(recentDelayMs = MAX_DELAY_BETWEEN_LOGS_MS), - dataPersistenceExecutorService: ExecutorService, - trackingConsentProvider: ConsentProvider -) : FilePersistenceStrategy( - File(context.filesDir, INTERMEDIATE_DATA_FOLDER), - File(context.filesDir, AUTHORIZED_FOLDER), - LogSerializer(), - dataPersistenceExecutorService, - filePersistenceConfig, - PayloadDecoration.JSON_ARRAY_DECORATION, - trackingConsentProvider -) { - companion object { - internal const val VERSION = 1 - internal const val ROOT = "dd-logs" - internal const val INTERMEDIATE_DATA_FOLDER = - "$ROOT-pending-v$VERSION" - internal const val AUTHORIZED_FOLDER = "$ROOT-v$VERSION" - internal const val MAX_DELAY_BETWEEN_LOGS_MS = 5000L - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogGenerator.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogGenerator.kt deleted file mode 100644 index 965c6699d1..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogGenerator.kt +++ /dev/null @@ -1,101 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import io.opentracing.util.GlobalTracer - -internal class LogGenerator( - internal val serviceName: String, - internal val loggerName: String, - internal val networkInfoProvider: NetworkInfoProvider?, - internal val userInfoProvider: UserInfoProvider, - envName: String, - appVersion: String -) { - - private val envTag: String? = if (envName.isNotEmpty()) { - "${LogAttributes.ENV}:$envName" - } else { - null - } - - private val appVersionTag = if (appVersion.isNotEmpty()) { - "${LogAttributes.APPLICATION_VERSION}:$appVersion" - } else { - null - } - - @Suppress("LongParameterList") - fun generateLog( - level: Int, - message: String, - throwable: Throwable?, - attributes: Map, - tags: Set, - timestamp: Long, - threadName: String? = null, - bundleWithTraces: Boolean = true, - bundleWithRum: Boolean = true - ): Log { - val combinedAttributes = resolveAttributes(attributes, bundleWithTraces, bundleWithRum) - val combinedTags = resolveTags(tags) - return Log( - serviceName = serviceName, - level = level, - message = message, - timestamp = timestamp, - throwable = throwable, - attributes = combinedAttributes, - tags = combinedTags.toList(), - networkInfo = networkInfoProvider?.getLatestNetworkInfo(), - userInfo = userInfoProvider.getUserInfo(), - loggerName = loggerName, - threadName = threadName ?: Thread.currentThread().name - ) - } - - private fun resolveTags( - tags: Set - ): MutableSet { - val combinedTags = mutableSetOf().apply { addAll(tags) } - envTag?.let { - combinedTags.add(it) - } - appVersionTag?.let { - combinedTags.add(it) - } - - return combinedTags - } - - private fun resolveAttributes( - attributes: Map, - bundleWithTraces: Boolean, - bundleWithRum: Boolean - ): MutableMap { - val combinedAttributes = mutableMapOf().apply { putAll(attributes) } - if (bundleWithTraces && GlobalTracer.isRegistered()) { - val tracer = GlobalTracer.get() - val activeContext = tracer.activeSpan()?.context() - if (activeContext != null) { - combinedAttributes[LogAttributes.DD_TRACE_ID] = activeContext.toTraceId() - combinedAttributes[LogAttributes.DD_SPAN_ID] = activeContext.toSpanId() - } - } - if (bundleWithRum && GlobalRum.isRegistered()) { - val activeContext = GlobalRum.getRumContext() - combinedAttributes[LogAttributes.RUM_APPLICATION_ID] = activeContext.applicationId - combinedAttributes[LogAttributes.RUM_SESSION_ID] = activeContext.sessionId - combinedAttributes[LogAttributes.RUM_VIEW_ID] = activeContext.viewId - } - return combinedAttributes - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogSerializer.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogSerializer.kt deleted file mode 100644 index 1842ff76a9..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/domain/LogSerializer.kt +++ /dev/null @@ -1,186 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import android.util.Log as AndroidLog -import com.datadog.android.BuildConfig -import com.datadog.android.core.internal.constraints.DataConstraints -import com.datadog.android.core.internal.constraints.DatadogDataConstraints -import com.datadog.android.core.internal.domain.Serializer -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.core.internal.utils.toJsonElement -import com.datadog.android.log.LogAttributes -import com.google.gson.JsonObject -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone - -/** - * The Logging feature implementation of the [Serializer] interface. - */ -internal class LogSerializer( - private val dataConstraints: DataConstraints = DatadogDataConstraints() -) : - Serializer { - - private val simpleDateFormat = SimpleDateFormat(ISO_8601, Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - - override fun serialize(model: Log): String { - return serializeLog(model) - } - - private fun serializeLog(log: Log): String { - val jsonLog = JsonObject() - - // Mandatory info - jsonLog.addProperty(LogAttributes.MESSAGE, log.message) - jsonLog.addProperty(LogAttributes.SERVICE_NAME, log.serviceName) - jsonLog.addProperty(LogAttributes.STATUS, resolveLogLevelStatus(log.level)) - jsonLog.addProperty(LogAttributes.LOGGER_NAME, log.loggerName) - jsonLog.addProperty(LogAttributes.LOGGER_THREAD_NAME, log.threadName) - jsonLog.addProperty(LogAttributes.LOGGER_VERSION, BuildConfig.VERSION_NAME) - - // Timestamp - val formattedDate = synchronized(simpleDateFormat) { - simpleDateFormat.format(Date(log.timestamp)) - } - jsonLog.addProperty(LogAttributes.DATE, formattedDate) - - // Network Info - addLogNetworkInfo(log, jsonLog) - - // User Info - addLogUserInfo(log, jsonLog) - - // Custom Attributes - addLogAttributes(log, jsonLog) - - // Tags - addLogTags(log, jsonLog) - - // Throwable - addLogThrowable(log, jsonLog) - - return jsonLog.toString() - } - - private fun addLogNetworkInfo( - log: Log, - jsonLog: JsonObject - ) { - val info = log.networkInfo - if (info != null) { - jsonLog.addProperty(LogAttributes.NETWORK_CONNECTIVITY, info.connectivity.serialized) - if (!info.carrierName.isNullOrBlank()) { - jsonLog.addProperty(LogAttributes.NETWORK_CARRIER_NAME, info.carrierName) - } - if (info.carrierId >= 0) { - jsonLog.addProperty(LogAttributes.NETWORK_CARRIER_ID, info.carrierId) - } - if (info.upKbps >= 0) { - jsonLog.addProperty(LogAttributes.NETWORK_UP_KBPS, info.upKbps) - } - if (info.downKbps >= 0) { - jsonLog.addProperty(LogAttributes.NETWORK_DOWN_KBPS, info.downKbps) - } - if (info.strength > Int.MIN_VALUE) { - jsonLog.addProperty(LogAttributes.NETWORK_SIGNAL_STRENGTH, info.strength) - } - } - } - - private fun addLogUserInfo(log: Log, jsonLog: JsonObject) { - val userInfo = log.userInfo - if (!userInfo.id.isNullOrEmpty()) { - jsonLog.addProperty(LogAttributes.USR_ID, userInfo.id) - } - if (!userInfo.name.isNullOrEmpty()) { - jsonLog.addProperty(LogAttributes.USR_NAME, userInfo.name) - } - if (!userInfo.email.isNullOrEmpty()) { - jsonLog.addProperty(LogAttributes.USR_EMAIL, userInfo.email) - } - // add extra info - dataConstraints.validateAttributes( - userInfo.extraInfo, - keyPrefix = LogAttributes.USR_ATTRIBUTES_GROUP, - attributesGroupName = USER_EXTRA_GROUP_VERBOSE_NAME - ).forEach { - val key = "${LogAttributes.USR_ATTRIBUTES_GROUP}.${it.key}" - jsonLog.add(key, it.value.toJsonElement()) - } - } - - private fun addLogThrowable( - log: Log, - jsonLog: JsonObject - ) { - log.throwable?.let { - jsonLog.addProperty(LogAttributes.ERROR_KIND, it.javaClass.simpleName) - jsonLog.addProperty(LogAttributes.ERROR_MESSAGE, it.message) - jsonLog.addProperty(LogAttributes.ERROR_STACK, it.loggableStackTrace()) - } - } - - private fun addLogTags( - log: Log, - jsonLog: JsonObject - ) { - val tags = dataConstraints.validateTags(log.tags) - .joinToString(",") - jsonLog.addProperty(TAG_DATADOG_TAGS, tags) - } - - private fun addLogAttributes( - log: Log, - jsonLog: JsonObject - ) { - dataConstraints.validateAttributes(log.attributes) - .filter { it.key.isNotBlank() && it.key !in reservedAttributes } - .forEach { - val jsonValue = it.value.toJsonElement() - jsonLog.add(it.key, jsonValue) - } - } - - companion object { - private const val ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - - internal const val TAG_DATADOG_TAGS = "ddtags" - internal const val USER_EXTRA_GROUP_VERBOSE_NAME = "user extra information" - - internal val reservedAttributes = arrayOf( - LogAttributes.HOST, - LogAttributes.MESSAGE, - LogAttributes.STATUS, - LogAttributes.SERVICE_NAME, - LogAttributes.SOURCE, - LogAttributes.ERROR_KIND, - LogAttributes.ERROR_MESSAGE, - LogAttributes.ERROR_STACK, - TAG_DATADOG_TAGS - ) - - internal fun resolveLogLevelStatus(level: Int): String { - return when (level) { - AndroidLog.ASSERT -> "critical" - AndroidLog.ERROR -> "error" - AndroidLog.WARN -> "warn" - AndroidLog.INFO -> "info" - AndroidLog.DEBUG -> "debug" - AndroidLog.VERBOSE -> "trace" - // If you change these you will have to propagate the changes - // also into the datadog-native-lib.cpp file inside the dd-sdk-android-ndk module. - Log.CRASH -> "emergency" - else -> "debug" - } - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandler.kt deleted file mode 100644 index 5706f28c97..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandler.kt +++ /dev/null @@ -1,27 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -internal class CombinedLogHandler( - internal vararg val handlers: LogHandler -) : LogHandler { - - // region LogHandler - - override fun handleLog( - level: Int, - message: String, - throwable: Throwable?, - attributes: Map, - tags: Set, - timestamp: Long? - ) { - handlers.forEach { it.handleLog(level, message, throwable, attributes, tags, timestamp) } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandler.kt deleted file mode 100644 index 23ee529279..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandler.kt +++ /dev/null @@ -1,25 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -internal class ConditionalLogHandler( - internal val delegateHandler: LogHandler, - internal val condition: (Int, Throwable?) -> Boolean -) : LogHandler { - override fun handleLog( - level: Int, - message: String, - throwable: Throwable?, - attributes: Map, - tags: Set, - timestamp: Long? - ) { - if (condition(level, throwable)) { - delegateHandler.handleLog(level, message, throwable, attributes, tags, timestamp) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandler.kt deleted file mode 100644 index 6aae79958e..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandler.kt +++ /dev/null @@ -1,73 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -import android.util.Log as AndroidLog -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.sampling.RateBasedSampler -import com.datadog.android.core.internal.sampling.Sampler -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.domain.LogGenerator -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource - -internal class DatadogLogHandler( - internal val logGenerator: LogGenerator, - internal val writer: Writer, - internal val bundleWithTraces: Boolean = true, - internal val bundleWithRum: Boolean = true, - internal val sampler: Sampler = RateBasedSampler(1.0f) -) : LogHandler { - - // region LogHandler - - override fun handleLog( - level: Int, - message: String, - throwable: Throwable?, - attributes: Map, - tags: Set, - timestamp: Long? - ) { - val resolvedTimeStamp = timestamp ?: System.currentTimeMillis() - if (sampler.sample()) { - val log = createLog(level, message, throwable, attributes, tags, resolvedTimeStamp) - writer.write(log) - } - - if (level >= AndroidLog.ERROR) { - GlobalRum.get().addError(message, RumErrorSource.LOGGER, throwable, attributes) - } - } - - // endregion - - // region Internal - - @Suppress("LongParameterList") - private fun createLog( - level: Int, - message: String, - throwable: Throwable?, - attributes: Map, - tags: Set, - timestamp: Long - ): Log { - return logGenerator.generateLog( - level, - message, - throwable, - attributes, - tags, - timestamp, - bundleWithRum = bundleWithRum, - bundleWithTraces = bundleWithTraces - ) - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/LogHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/LogHandler.kt deleted file mode 100644 index 7ca1cdad38..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/LogHandler.kt +++ /dev/null @@ -1,29 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface LogHandler { - - /** - * Handle the log. - * @param message the message to be logged - * @param throwable a (nullable) throwable to be logged with the message - * @param attributes a map of attributes to include only for this message. If an attribute with - * the same key already exists in this logger, it will be overridden (just for this message) - */ - fun handleLog( - level: Int, - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap(), - tags: Set = emptySet(), - timestamp: Long? = null - ) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandler.kt deleted file mode 100644 index 4ec412521f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandler.kt +++ /dev/null @@ -1,98 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -import android.os.Build -import android.util.Log -import com.datadog.android.Datadog -import com.datadog.android.log.Logger - -internal class LogcatLogHandler( - internal val serviceName: String, - internal val useClassnameAsTag: Boolean -) : LogHandler { - - // region LogHandler - - override fun handleLog( - level: Int, - message: String, - throwable: Throwable?, - attributes: Map, - tags: Set, - timestamp: Long? - ) { - val stackElement = getCallerStackElement() - val tag = resolveTag(stackElement) - val suffix = resolveSuffix(stackElement) - Log.println(level, tag, message + suffix) - if (throwable != null) { - Log.println( - level, - tag, - Log.getStackTraceString(throwable) - ) - } - } - - // endregion - - // region Internal - - internal fun resolveTag(stackTraceElement: StackTraceElement?): String { - val tag = if (stackTraceElement == null) { - serviceName - } else { - stackTraceElement.className - .replace(ANONYMOUS_CLASS, "") - .substringAfterLast('.') - } - return if (tag.length >= MAX_TAG_LENGTH && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - tag.substring(0, MAX_TAG_LENGTH) - } else { - tag - } - } - - private fun resolveSuffix(stackTraceElement: StackTraceElement?): String { - return if (stackTraceElement == null) { - "" - } else { - "\t| at .${stackTraceElement.methodName}" + - "(${stackTraceElement.fileName}:${stackTraceElement.lineNumber})" - } - } - - internal fun getCallerStackElement(): StackTraceElement? { - return if (Datadog.isDebug && useClassnameAsTag) { - val stackTrace = Throwable().stackTrace - stackTrace.firstOrNull { - it.className !in ignoredClassNames - } - } else { - null - } - } - - // endregion - - companion object { - - private const val MAX_TAG_LENGTH = 23 - - private val ANONYMOUS_CLASS = Regex("(\\$\\d+)+$") - private val ignoredClassNames = arrayOf( - Logger::class.java.canonicalName, - LogHandler::class.java.canonicalName, - LogHandler::class.java.canonicalName + "\$DefaultImpls", - LogcatLogHandler::class.java.canonicalName, - ConditionalLogHandler::class.java.canonicalName, - CombinedLogHandler::class.java.canonicalName, - DatadogLogHandler::class.java.canonicalName - ) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/net/LogsOkHttpUploader.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/net/LogsOkHttpUploader.kt deleted file mode 100644 index a0ffcdb07c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/net/LogsOkHttpUploader.kt +++ /dev/null @@ -1,46 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.net - -import com.datadog.android.core.internal.net.DataOkHttpUploader -import java.util.Locale -import okhttp3.OkHttpClient - -internal open class LogsOkHttpUploader( - endpoint: String, - private val token: String, - client: OkHttpClient -) : DataOkHttpUploader(buildUrl(endpoint, token), client) { - - // region DataOkHttpUploader - - override fun setEndpoint(endpoint: String) { - super.setEndpoint(buildUrl(endpoint, token)) - } - - override fun buildQueryParams(): Map { - return mutableMapOf( - QP_BATCH_TIME to System.currentTimeMillis(), - QP_SOURCE to DD_SOURCE_ANDROID - ) - } - - // endregion - - companion object { - internal const val UPLOAD_URL = "%s/v1/input/%s" - - private fun buildUrl(endpoint: String, token: String): String { - return String.format( - Locale.US, - UPLOAD_URL, - endpoint, - token - ) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/DatadogUserInfoProvider.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/DatadogUserInfoProvider.kt deleted file mode 100644 index 27146de50f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/DatadogUserInfoProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.user - -internal class DatadogUserInfoProvider : MutableUserInfoProvider { - - private var internalUserInfo = UserInfo() - - override fun setUserInfo(userInfo: UserInfo) { - internalUserInfo = userInfo - } - - override fun getUserInfo(): UserInfo { - return internalUserInfo - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/MutableUserInfoProvider.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/MutableUserInfoProvider.kt deleted file mode 100644 index 87bf2ed620..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/MutableUserInfoProvider.kt +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.user - -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface MutableUserInfoProvider : UserInfoProvider { - - fun setUserInfo(userInfo: UserInfo) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/UserInfo.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/UserInfo.kt deleted file mode 100644 index 5bde2bf2ed..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/UserInfo.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.user - -internal data class UserInfo( - val id: String? = null, - val name: String? = null, - val email: String? = null, - val extraInfo: Map = emptyMap() -) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/UserInfoProvider.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/UserInfoProvider.kt deleted file mode 100644 index f7e496d745..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/log/internal/user/UserInfoProvider.kt +++ /dev/null @@ -1,12 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.user - -internal interface UserInfoProvider { - - fun getUserInfo(): UserInfo -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogContext.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogContext.kt deleted file mode 100644 index 809de99b11..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogContext.kt +++ /dev/null @@ -1,15 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.plugin - -/** - * Provides general information about the current context of the library. - * @see DatadogRumContext - */ -data class DatadogContext( - val rum: DatadogRumContext? = null -) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogPlugin.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogPlugin.kt deleted file mode 100644 index 21a10eadab..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogPlugin.kt +++ /dev/null @@ -1,41 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.plugin - -import com.datadog.android.privacy.TrackingConsentProviderCallback - -/** - * DatadogPlugin interface. You can attach as many as you want for any existing feature in the - * SDK. - * @see [Feature.LOG] - * @see [Feature.CRASH] - * @see [Feature.TRACE] - * @see [Feature.RUM] - */ -interface DatadogPlugin : TrackingConsentProviderCallback { - - /** - * Registers this plugin. This will be called when the feature for which this plugin - * was assigned will be initialised. - * @param config the [DatadogPluginConfig] - */ - fun register(config: DatadogPluginConfig) - - /** - * Unregisters this plugin. This will be called when the feature for which this plugin - * was assigned will be stopped. - */ - fun unregister() - - /** - * Notify that the current context of the library was updated by one or more of the features. - * This method is always called from a worker thread. - * @param context the updated [DatadogContext]. - * - */ - fun onContextChanged(context: DatadogContext) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogPluginConfig.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogPluginConfig.kt deleted file mode 100644 index 6c5842de39..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogPluginConfig.kt +++ /dev/null @@ -1,82 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.plugin - -import android.content.Context -import com.datadog.android.error.internal.CrashLogFileStrategy -import com.datadog.android.log.internal.domain.LogFileStrategy -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.rum.internal.domain.RumFileStrategy -import com.datadog.android.tracing.internal.domain.TracingFileStrategy - -/** - * Used to deliver the context from the SDK internals to a [DatadogPlugin] implementation. - */ -sealed class DatadogPluginConfig( - val context: Context, - val envName: String, - val serviceName: String, - val featurePersistenceDirName: String, - val trackingConsent: TrackingConsent -) { - - internal class CrashReportsPluginConfig( - context: Context, - envName: String, - serviceName: String, - trackingConsent: TrackingConsent - ) : - DatadogPluginConfig( - context, - envName, - serviceName, - CrashLogFileStrategy.AUTHORIZED_FOLDER, - trackingConsent - ) - - internal class LogsPluginConfig( - context: Context, - envName: String, - serviceName: String, - trackingConsent: TrackingConsent - ) : - DatadogPluginConfig( - context, - envName, - serviceName, - LogFileStrategy.AUTHORIZED_FOLDER, - trackingConsent - ) - - internal class TracingPluginConfig( - context: Context, - envName: String, - serviceName: String, - trackingConsent: TrackingConsent - ) : - DatadogPluginConfig( - context, - envName, - serviceName, - TracingFileStrategy.AUTHORIZED_FOLDER, - trackingConsent - ) - - internal class RumPluginConfig( - context: Context, - envName: String, - serviceName: String, - trackingConsent: TrackingConsent - ) : - DatadogPluginConfig( - context, - envName, - serviceName, - RumFileStrategy.AUTHORIZED_FOLDER, - trackingConsent - ) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogRumContext.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogRumContext.kt deleted file mode 100644 index d8545aa2f7..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/DatadogRumContext.kt +++ /dev/null @@ -1,19 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.plugin - -/** - * Provides information on the RUM feature context. - * @param applicationId the RUM Application ID provided when initialising the SDK. - * @param sessionId the unique ID of the current RUM Session. - * @param viewId the unique ID of the current tracked RUM View. - */ -data class DatadogRumContext( - val applicationId: String? = null, - val sessionId: String? = null, - val viewId: String? = null -) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/Feature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/Feature.kt deleted file mode 100644 index ae9331a489..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/plugin/Feature.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.plugin - -/** - * Provides the available feature for which a [DatadogPlugin] can be assigned. - */ -enum class Feature { - LOG, - CRASH, - TRACE, - RUM -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/GlobalRum.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/GlobalRum.kt deleted file mode 100644 index ae0f864f60..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/GlobalRum.kt +++ /dev/null @@ -1,179 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.error.internal.CrashReportsFeature -import com.datadog.android.log.internal.LogsFeature -import com.datadog.android.plugin.DatadogContext -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.DatadogRumContext -import com.datadog.android.rum.GlobalRum.get -import com.datadog.android.rum.GlobalRum.registerIfAbsent -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.tracing.internal.TracesFeature -import java.util.concurrent.Callable -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.atomic.AtomicReference - -/** - * A global [RumMonitor] holder, ensuring that all RUM events are registered - * on the same instance . - * - * The [registerIfAbsent] method should only be called once during the application - * initialization phase. Any following calls will be no-op. If the [registerIfAbsent] - * method is never called, a default no-op implementation is used. - * - * You can then retrieve the active [RumMonitor] using the [get] method. - */ -object GlobalRum { - - internal val globalAttributes: MutableMap = ConcurrentHashMap() - - internal val sessionStartNs = AtomicLong(0L) - - internal val isRegistered = AtomicBoolean(false) - internal var monitor: RumMonitor = NoOpRumMonitor() - - private var activeContext: AtomicReference = AtomicReference(RumContext()) - - /** - * Identify whether a [RumMonitor] has previously been registered. - * - * This check is useful in scenarios where more than one component may be responsible - * for registering a monitor. - * - * @return whether a monitor has been registered - * @see [registerIfAbsent] - */ - @JvmStatic - fun isRegistered(): Boolean { - return isRegistered.get() - } - - /** - * Register a [RumMonitor] to back the behaviour of the [get]. - * - * Registration is a one-time operation. Once a monitor has been registered, all attempts at re-registering - * will return `false`. - * - * Every application intending to use the global monitor is responsible for registering it once - * during its initialization. - * - * @param monitor the monitor to use as global monitor. - * @return `true` if the provided monitor was registered as a result of this call, `false` otherwise. - */ - @JvmStatic - fun registerIfAbsent(monitor: RumMonitor): Boolean { - return registerIfAbsent(Callable { monitor }) - } - - /** - * Register a [RumMonitor] to back the behaviour of the [get]. - * - * The monitor is provided through a [Callable] that will only be called if the global monitor is absent. - * Registration is a one-time operation. Once a monitor has been registered, all attempts at re-registering - * will return `false`. - * - * Every application intending to use the global monitor is responsible for registering it once - * during its initialization. - * - * @param provider Provider for the monitor to use as global monitor. - * @return `true` if the provided monitor was registered as a result of this call, `false` otherwise. - */ - @JvmStatic - fun registerIfAbsent(provider: Callable): Boolean { - if (isRegistered.get()) { - devLogger.w("RumMonitor has already been registered") - return false - } else { - if (isRegistered.compareAndSet(false, true)) { - monitor = provider.call() - return true - } else { - return false - } - } - } - - /** - * Returns the constant [RumMonitor] instance. - * - * Until a monitor is explicitly configured with [registerIfAbsent], - * a no-op implementation is returned. - * - * @return The global monitor instance. - * @see [registerIfAbsent] - */ - @JvmStatic - fun get(): RumMonitor { - return monitor - } - - /** - * Adds a global attribute to all future RUM events. - * @param key the attribute key (non null) - * @param value the attribute value (or null) - */ - @JvmStatic - fun addAttribute(key: String, value: Any?) { - globalAttributes[key] = value - } - - /** - * Removes a global attribute from all future RUM events. - * @param key the attribute key (non null) - */ - @JvmStatic - fun removeAttribute(key: String) { - globalAttributes.remove(key) - } - - // region Internal - - internal fun getRumContext(): RumContext { - return activeContext.get() - } - - internal fun updateRumContext(newContext: RumContext) { - activeContext.set(newContext) - val pluginContext = DatadogContext( - DatadogRumContext( - newContext.applicationId, - newContext.sessionId, - newContext.viewId - ) - ) - updateContextInPlugins(pluginContext, RumFeature.getPlugins()) - updateContextInPlugins(pluginContext, CrashReportsFeature.getPlugins()) - updateContextInPlugins(pluginContext, LogsFeature.getPlugins()) - updateContextInPlugins(pluginContext, TracesFeature.getPlugins()) - } - - private fun updateContextInPlugins( - pluginContext: DatadogContext, - plugins: List - ) { - plugins.forEach { - it.onContextChanged(pluginContext) - } - } - - // For Tests only - - @Suppress("unused") - @JvmStatic - private fun resetSession() { - (monitor as? AdvancedRumMonitor)?.resetSession() - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt deleted file mode 100644 index c8f3cf61a3..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt +++ /dev/null @@ -1,206 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import com.datadog.android.DatadogConfig -import com.datadog.android.DatadogInterceptor - -/** - * This class holds constant rum attribute keys. - */ -// @Suppress("unused") -object RumAttributes { - - // region Tags - - /** - * The human readable version of the application. (String) - * This value is extracted from your application's manifest and - * filled automatically by the [RumMonitor]. - */ - const val APPLICATION_VERSION: String = "version" - - /** - * The custom environment name. (Number) - * This value is filled automatically by the [RumMonitor]. - */ - const val ENV: String = "env" - - /** - * The name of the application or service generating the rum events. (String) - * This values is configurable through the DatadogConfig during the SDK initialization. - * By default it will take the application package name. - * @see [DatadogConfig.Builder.setServiceName] - */ - const val SERVICE_NAME: String = "service" - - /** - * The technology from which the log originated. (String) - * This value is filled automatically by the [RumMonitor]. - */ - const val SOURCE: String = "source" - - /** - * Version of the current Datadog SDK. - */ - const val SDK_VERSION: String = "sdk_version" - - // endregion - - // region Resource - - /** - * Trace Id related to the resource loading. (Number) - * This value is filled automatically by the [DatadogInterceptor]. - */ - const val TRACE_ID: String = "_dd.trace_id" - - /** - * Span Id related to the resource loading. (Number) - * This value is filled automatically by the [DatadogInterceptor]. - */ - const val SPAN_ID: String = "_dd.span_id" - - // endregion - - // region Error - - /** - * Indicates the action performed by the Resource which triggered the error. (String) - * This value is filled automatically by the [RumMonitor] and the [RumInterceptor]. - * @see [RumMonitor.startResource] - */ - const val ERROR_RESOURCE_METHOD: String = "error.resource.method" - - /** - * The HTTP response status code for the Resource which triggered the error. (Number) - * This value is filled automatically by the [RumInterceptor]. - * @see [RumMonitor.stopResourceWithError] - */ - const val ERROR_RESOURCE_STATUS_CODE: String = "error.resource.status_code" - - /** - * The URL of a loaded Resource which triggered the error. (String) - * This value is filled automatically by the [RumMonitor] and the [RumInterceptor]. - * @see [RumMonitor.stopResourceWithError] - */ - const val ERROR_RESOURCE_URL: String = "error.resource.url" - - /** - * The version of the Database that triggered the error. - * This value is filled automatically by the [RumMonitor]. - */ - const val ERROR_DATABASE_VERSION: String = "error.database.version" - - /** - * The path of the Database that triggered the error. - * This value is filled automatically by the [RumMonitor]. - */ - const val ERROR_DATABASE_PATH: String = "error.database.path" - - // endregion - - // region Action - - /** - * The touch target class name. (String) - * @see [RumMonitor.addAction] - * @see [RumMonitor.startAction] - * @see [RumMonitor.stopAction] - */ - const val ACTION_TARGET_CLASS_NAME: String = "action.target.classname" - - /** - * The title of the action's target. (String) - * @see [RumMonitor.addAction] - * @see [RumMonitor.startAction] - * @see [RumMonitor.stopAction] - */ - const val ACTION_TARGET_TITLE: String = "action.target.title" - - /** - * The index of the touch target in the parent view. (Integer) - * For now we only detect RecyclerView as parent. - */ - const val ACTION_TARGET_PARENT_INDEX: String = "action.target.parent.index" - - /** - * The class name of the touch target's parent view. (String) - * For now we only detect RecyclerView as parent. - */ - const val ACTION_TARGET_PARENT_CLASSNAME: String = "action.target.parent.classname" - - /** - * The resource id of the target container in case this is a scrollable component. (String) - * In case the resource id is missing we will provide the - * container id in a Hexa String format (e.g. 0x1A2B1) - * For now we only support the RecyclerView component. - */ - const val ACTION_TARGET_PARENT_RESOURCE_ID: String = "action.target.parent.resource_id" - - /** - * The touch target resource id. (String) - * It can either be the resource identifier, or the raw hexadecimal value. - * @see [RumMonitor.addAction] - * @see [RumMonitor.startAction] - * @see [RumMonitor.stopAction] - */ - const val ACTION_TARGET_RESOURCE_ID: String = "action.target.resource_id" - - /** - * The gesture event direction. - */ - const val ACTION_GESTURE_DIRECTION: String = "action.gesture.direction" - - // endregion - - // region Network Info - - /** - * The unique id of the Carrier attached to the SIM card. (Number) - * This value is filled automatically by the [RumMonitor] for resources and errors. - */ - const val NETWORK_CARRIER_ID: String = "network.client.sim_carrier.id" - - /** - * The name of the Carrier attached to the SIM card. (String) - * This value is filled automatically by the [RumMonitor] for resources and errors. - */ - const val NETWORK_CARRIER_NAME: String = "network.client.sim_carrier.name" - - /** - * The connectivity status of the device. (String) - * This value is filled automatically by the [RumMonitor] for resources and errors. - */ - const val NETWORK_CONNECTIVITY: String = "network.client.connectivity" - - /** - * The downstream bandwidth for the current network in Kbps. (Number) - * This value is filled automatically by the [RumMonitor] for resources and errors. - */ - const val NETWORK_DOWN_KBPS: String = "network.client.downlink_kbps" - - /** - * The bearer specific signal strength. (Number) - * This value is filled automatically by the [RumMonitor] for resources and errors. - */ - const val NETWORK_SIGNAL_STRENGTH: String = "network.client.signal_strength" - - /** - * The upstream bandwidth for the current network in Kbps. (Number) - * This value is filled automatically by the [RumMonitor] for resources and errors. - */ - const val NETWORK_UP_KBPS: String = "network.client.uplink_kbps" - - /** - * Total number of bytes transmitted from the client to the server. (Number) - * TODO RUMM-469 rename to v2 - */ - const val NETWORK_BYTES_READ: String = "network.bytes_read" - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumInterceptor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumInterceptor.kt deleted file mode 100644 index 4f9d95c82d..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumInterceptor.kt +++ /dev/null @@ -1,35 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import com.datadog.android.DatadogConfig -import com.datadog.android.DatadogInterceptor -import com.datadog.android.rum.tracking.ViewTrackingStrategy -import okhttp3.Interceptor -import okhttp3.OkHttpClient - -/** - * Provides automatic RUM integration for [OkHttpClient] by way of the [Interceptor] system. - * - * This interceptor will log the request as a RUM Resource, and fill the request information - * (url, method, status code, optional error). Note that RUM Resources are only tracked when a - * view is active. You can use one of the existing [ViewTrackingStrategy] when configuring the SDK - * (see [DatadogConfig.Builder.useViewTrackingStrategy]) or start a view manually (see - * [RumMonitor.startView]). - * - * If you use multiple Interceptors, make sure that this one is called first. - * If you also want to trace network request, use the - * [DatadogInterceptor] instead, which combines the RUM and APM integrations. - * - * To use: - * ``` - * OkHttpClient client = new OkHttpClient.Builder() - * .addInterceptor(new RumInterceptor()) - * .build(); - * ``` - */ -class RumInterceptor : DatadogInterceptor(listOf("")) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt deleted file mode 100644 index 87e3247b1d..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt +++ /dev/null @@ -1,237 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import android.app.Activity -import android.app.Fragment -import android.os.Handler -import android.os.Looper -import androidx.annotation.FloatRange -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.monitor.DatadogRumMonitor -import com.datadog.tools.annotation.NoOpImplementation - -/** - * A class enabling Datadog RUM features. - * - * It allows you to record User events that can be explored and analyzed in Datadog Dashboards. - * - * You can only have one active RumMonitor, and should register/retrieve it from the [GlobalRum] object. - */ -@NoOpImplementation -@SuppressWarnings("ComplexInterface") -interface RumMonitor { - - /** - * Notifies that a View is being shown to the user, linked with the [key] instance. - * @param key the instance that represents the active view (usually your - * [Activity] or [Fragment] instance). - * @param name the name of the view (usually your [Activity] or [Fragment] full class name) - * @param attributes additional custom attributes to attach to the view. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - * @see [stopView] - */ - fun startView( - key: Any, - name: String, - attributes: Map = emptyMap() - ) - - /** - * Stops a previously started View, linked with the [key] instance. - * @param key the instance that represents the active view (usually your - * [Activity] or [Fragment] instance). - * @param attributes additional custom attributes to attach to the view. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - * @see [startView] - */ - fun stopView( - key: Any, - attributes: Map = emptyMap() - ) - - /** - * Notifies that a User Action happened. - * This is used to track discrete user actions (e.g.: tap). - * @param type the action type - * @param name the action identifier - * @param attributes additional custom attributes to attach to the action. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - * @see [startUserAction] - * @see [stopUserAction] - */ - fun addUserAction( - type: RumActionType, - name: String, - attributes: Map - ) - - /** - * Notifies that a User Action started. - * This is used to track long running user actions (e.g.: scroll). Such a user action must - * be stopped with [stopUserAction], and will be stopped automatically if it lasts more than - * 10 seconds. - * @param type the action type - * @param name the action identifier - * @param attributes additional custom attributes to attach to the action. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - * @see [stopUserAction] - * @see [addUserAction] - */ - fun startUserAction( - type: RumActionType, - name: String, - attributes: Map - ) - - /** - * Notifies that a User Action stopped. - * This is used to stop tracking long running user actions (e.g.: scroll), started - * with [startUserAction]. - * @param type the action type (overriding the last started action) - * @param name the action identifier (overriding the last started action) - * @param attributes additional custom attributes to attach to the action. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - * @see [addUserAction] - * @see [startUserAction] - */ - fun stopUserAction( - type: RumActionType, - name: String, - attributes: Map = emptyMap() - ) - - /** - * Notify that a new Resource is being loaded, linked with the [key] instance. - * @param key the instance that represents the resource being loaded (usually your - * request or network call instance). - * @param method the method used to load the resource (E.g., for network: "GET" or "POST") - * @param url the url or local path of the resource being loaded - * @param attributes additional custom attributes to attach to the resource. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - * @see [stopResource] - * @see [stopResourceWithError] - */ - fun startResource( - key: String, - method: String, - url: String, - attributes: Map = emptyMap() - ) - - /** - * Stops a previously started Resource, linked with the [key] instance. - * @param key the instance that represents the active view (usually your - * request or network call instance). - * @param statusCode the status code of the resource (if any) - * @param size the size of the resource, in bytes - * @param kind the type of resource loaded - * @param attributes additional custom attributes to attach to the resource. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - * @see [startResource] - * @see [stopResourceWithError] - */ - fun stopResource( - key: String, - statusCode: Int?, - size: Long?, - kind: RumResourceKind, - attributes: Map - ) - - /** - * Stops a previously started Resource that failed loading, linked with the [key] instance. - * @param key the instance that represents the active view (usually your - * request or network call instance). - * @param statusCode the status code of the resource (if any) - * @param message a message explaining the error - * @param source the source of the error - * @param throwable the throwable - * @see [startResource] - * @see [stopResource] - */ - fun stopResourceWithError( - key: String, - statusCode: Int?, - message: String, - source: RumErrorSource, - throwable: Throwable - ) - - /** - * Notifies that an error occurred in the active View. - * @param message a message explaining the error - * @param source the source of the error - * @param throwable the throwable - * @param attributes additional custom attributes to attach to the error. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. - */ - fun addError( - message: String, - source: RumErrorSource, - throwable: Throwable?, - attributes: Map - ) - - /** - * Adds a specific timing in the active View. The timing duration will be computed as the - * difference between the time the View was started and the time this function was called. - * @param name the name of the new custom timing attribute. Timings can be - * nested up to 8 levels deep. Names using more than 8 levels will be sanitized by SDK. - */ - fun addTiming( - name: String - ) - - // region Builder - - /** - * A Builder class for a [RumMonitor]. - */ - class Builder { - - private var samplingRate: Float = RumFeature.samplingRate - - /** - * Sets the sampling rate for RUM Sessions. - * - * @param samplingRate the sampling rate must be a value between 0 and 100. A value of 0 - * means no RUM event will be sent, 100 means all sessions will be kept. - */ - fun sampleRumSessions(@FloatRange(from = 0.0, to = 100.0) samplingRate: Float): Builder { - this.samplingRate = samplingRate - return this - } - - /** - * Builds a [RumMonitor] based on the current state of this Builder. - */ - fun build(): RumMonitor { - return if (!RumFeature.isInitialized()) { - devLogger.e(RUM_NOT_ENABLED_ERROR_MESSAGE) - NoOpRumMonitor() - } else { - DatadogRumMonitor( - applicationId = RumFeature.applicationId, - samplingRate = samplingRate, - writer = RumFeature.persistenceStrategy.getWriter(), - handler = Handler(Looper.getMainLooper()), - firstPartyHostDetector = CoreFeature.firstPartyHostDetector - ) - } - } - - companion object { - internal const val RUM_NOT_ENABLED_ERROR_MESSAGE = - "You're trying to create a RumMonitor instance, " + - "but the RUM feature was disabled in your DatadogConfig. " + - "No RUM data will be sent." - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumResourceKind.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumResourceKind.kt deleted file mode 100644 index 46315779fd..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/RumResourceKind.kt +++ /dev/null @@ -1,49 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import java.util.Locale - -/** - * Describe the category of a RUM Resource. - * @see [RumMonitor] - */ -enum class RumResourceKind(val value: String) { - // Specific kind of JS resources loading - BEACON("beacon"), - FETCH("fetch"), - XHR("xhr"), - DOCUMENT("document"), - - // Common kinds - UNKNOWN("unknown"), - IMAGE("image"), - JS("js"), - FONT("font"), - CSS("css"), - MEDIA("media"), - OTHER("other"); - - companion object { - - @JvmStatic - internal fun fromMimeType(mimeType: String): RumResourceKind { - val baseType = mimeType.substringBefore('/').toLowerCase(Locale.US) - val subtype = mimeType.substringAfter('/').substringBefore(';').toLowerCase(Locale.US) - - return when { - baseType == "image" -> IMAGE - baseType == "video" || baseType == "audio" -> MEDIA - baseType == "font" -> FONT - baseType == "text" && subtype == "css" -> CSS - baseType == "text" && subtype == "javascript" -> JS - mimeType.isBlank() -> UNKNOWN - else -> OTHER - } - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt deleted file mode 100644 index 26ca5feabb..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt +++ /dev/null @@ -1,191 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal - -import android.content.Context -import com.datadog.android.DatadogConfig -import com.datadog.android.DatadogEndpoint -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.SdkFeature -import com.datadog.android.core.internal.data.upload.DataUploadScheduler -import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler -import com.datadog.android.core.internal.data.upload.UploadScheduler -import com.datadog.android.core.internal.domain.NoOpPersistenceStrategy -import com.datadog.android.core.internal.domain.PersistenceStrategy -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.NoOpDataUploader -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.log.internal.user.NoOpMutableUserInfoProvider -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.plugin.DatadogPluginConfig -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.internal.domain.RumFileStrategy -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.rum.internal.instrumentation.gestures.NoOpGesturesTracker -import com.datadog.android.rum.internal.monitor.DatadogRumMonitor -import com.datadog.android.rum.internal.net.RumOkHttpUploader -import com.datadog.android.rum.internal.tracking.NoOpUserActionTrackingStrategy -import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy -import com.datadog.android.rum.internal.tracking.ViewTreeChangeTrackingStrategy -import com.datadog.android.rum.tracking.NoOpViewTrackingStrategy -import com.datadog.android.rum.tracking.TrackingStrategy -import com.datadog.android.rum.tracking.ViewTrackingStrategy -import java.util.UUID -import java.util.concurrent.ExecutorService -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.atomic.AtomicBoolean -import okhttp3.OkHttpClient - -internal object RumFeature : SdkFeature() { - - internal val initialized = AtomicBoolean(false) - - internal var clientToken: String = "" - internal var endpointUrl: String = DatadogEndpoint.RUM_US - internal var envName: String = "" - internal var applicationId: UUID = UUID(0, 0) - internal var samplingRate: Float = 0f - - internal var persistenceStrategy: PersistenceStrategy = NoOpPersistenceStrategy() - internal var uploader: DataUploader = NoOpDataUploader() - internal var dataUploadScheduler: UploadScheduler = NoOpUploadScheduler() - internal var userInfoProvider: UserInfoProvider = NoOpMutableUserInfoProvider() - internal var networkInfoProvider: NetworkInfoProvider = NoOpNetworkInfoProvider() - - internal var gesturesTracker: GesturesTracker = NoOpGesturesTracker() - private var viewTrackingStrategy: ViewTrackingStrategy = NoOpViewTrackingStrategy() - private var actionTrackingStrategy: UserActionTrackingStrategy = - NoOpUserActionTrackingStrategy() - private var viewTreeTrackingStrategy: TrackingStrategy = ViewTreeChangeTrackingStrategy() - - @Suppress("LongParameterList") - fun initialize( - appContext: Context, - config: DatadogConfig.RumConfig, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - systemInfoProvider: SystemInfoProvider, - dataUploadThreadPoolExecutor: ScheduledThreadPoolExecutor, - dataPersistenceExecutor: ExecutorService, - userInfoProvider: UserInfoProvider, - trackingConsentProvider: ConsentProvider - ) { - if (initialized.get()) { - return - } - - applicationId = config.applicationId - clientToken = config.clientToken - endpointUrl = config.endpointUrl - envName = config.envName - samplingRate = config.samplingRate - - config.gesturesTracker?.let { gesturesTracker = it } - config.viewTrackingStrategy?.let { viewTrackingStrategy = it } - config.userActionTrackingStrategy?.let { actionTrackingStrategy = it } - - persistenceStrategy = RumFileStrategy( - appContext, - trackingConsentProvider = trackingConsentProvider, - dataPersistenceExecutorService = dataPersistenceExecutor - ) - setupUploader( - endpointUrl, - okHttpClient, - networkInfoProvider, - systemInfoProvider, - dataUploadThreadPoolExecutor = dataUploadThreadPoolExecutor - ) - registerTrackingStrategies(appContext) - this.userInfoProvider = userInfoProvider - this.networkInfoProvider = networkInfoProvider - registerPlugins( - config.plugins, - DatadogPluginConfig.RumPluginConfig( - appContext, - config.envName, - CoreFeature.serviceName, - trackingConsentProvider.getConsent() - ), - trackingConsentProvider - ) - initialized.set(true) - } - - fun isInitialized(): Boolean { - return initialized.get() - } - - fun clearAllData() { - persistenceStrategy.clearAllData() - } - - fun stop() { - if (initialized.get()) { - unregisterPlugins() - dataUploadScheduler.stopScheduling() - - unregisterTrackingStrategies(CoreFeature.contextRef.get()) - - persistenceStrategy = NoOpPersistenceStrategy() - dataUploadScheduler = NoOpUploadScheduler() - clientToken = "" - endpointUrl = DatadogEndpoint.RUM_US - envName = "" - - (GlobalRum.get() as? DatadogRumMonitor)?.stopKeepAliveCallback() - // reset rum monitor to NoOp and reset the flag - GlobalRum.isRegistered.set(false) - GlobalRum.registerIfAbsent(NoOpRumMonitor()) - GlobalRum.isRegistered.set(false) - initialized.set(false) - } - } - - // region Internal - - private fun setupUploader( - endpointUrl: String, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - systemInfoProvider: SystemInfoProvider, - dataUploadThreadPoolExecutor: ScheduledThreadPoolExecutor - ) { - dataUploadScheduler = if (CoreFeature.isMainProcess) { - uploader = RumOkHttpUploader(endpointUrl, clientToken, okHttpClient) - DataUploadScheduler( - persistenceStrategy.getReader(), - uploader, - networkInfoProvider, - systemInfoProvider, - dataUploadThreadPoolExecutor - ) - } else { - NoOpUploadScheduler() - } - dataUploadScheduler.startScheduling() - } - - private fun registerTrackingStrategies(appContext: Context) { - actionTrackingStrategy.register(appContext) - viewTrackingStrategy.register(appContext) - viewTreeTrackingStrategy.register(appContext) - } - - private fun unregisterTrackingStrategies(appContext: Context?) { - actionTrackingStrategy.unregister(appContext) - viewTrackingStrategy.unregister(appContext) - viewTreeTrackingStrategy.unregister(appContext) - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/RumContext.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/RumContext.kt deleted file mode 100644 index 37bca37e93..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/RumContext.kt +++ /dev/null @@ -1,22 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain - -import java.util.UUID - -internal data class RumContext( - val applicationId: String = NULL_UUID, - val sessionId: String = NULL_UUID, - val viewId: String? = null, - val viewUrl: String? = null, - val actionId: String? = null -) { - - companion object { - val NULL_UUID = UUID(0, 0).toString() - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/RumFileStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/RumFileStrategy.kt deleted file mode 100644 index 6246c51b80..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/RumFileStrategy.kt +++ /dev/null @@ -1,42 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain - -import android.content.Context -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.FilePersistenceStrategy -import com.datadog.android.core.internal.domain.PayloadDecoration -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.event.RumEventSerializer -import java.io.File -import java.util.concurrent.ExecutorService - -internal class RumFileStrategy( - context: Context, - filePersistenceConfig: FilePersistenceConfig = - FilePersistenceConfig(recentDelayMs = MAX_DELAY_BETWEEN_RUM_EVENTS_MS), - dataPersistenceExecutorService: ExecutorService, - trackingConsentProvider: ConsentProvider -) : FilePersistenceStrategy( - File(context.filesDir, INTERMEDIATE_DATA_FOLDER), - File(context.filesDir, AUTHORIZED_FOLDER), - RumEventSerializer(), - dataPersistenceExecutorService, - filePersistenceConfig, - PayloadDecoration.NEW_LINE_DECORATION, - trackingConsentProvider -) { - companion object { - internal const val VERSION = 1 - internal const val ROOT = "dd-rum" - internal const val INTERMEDIATE_DATA_FOLDER = - "$ROOT-pending-v$VERSION" - internal const val AUTHORIZED_FOLDER = "$ROOT-v$VERSION" - internal const val MAX_DELAY_BETWEEN_RUM_EVENTS_MS = 5000L - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/ResourceTiming.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/ResourceTiming.kt deleted file mode 100644 index 11e0aaf198..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/ResourceTiming.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.event - -internal data class ResourceTiming( - val dnsStart: Long = 0L, - val dnsDuration: Long = 0L, - val connectStart: Long = 0L, - val connectDuration: Long = 0L, - val sslStart: Long = 0L, - val sslDuration: Long = 0L, - val firstByteStart: Long = 0L, - val firstByteDuration: Long = 0L, - val downloadStart: Long = 0L, - val downloadDuration: Long = 0L -) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEvent.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEvent.kt deleted file mode 100644 index 3880566c63..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.event - -internal data class RumEvent( - val event: Any, - val globalAttributes: Map, - val userExtraAttributes: Map, - val customTimings: Map? = null -) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt deleted file mode 100644 index 21c072d802..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt +++ /dev/null @@ -1,115 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.event - -import com.datadog.android.core.internal.constraints.DataConstraints -import com.datadog.android.core.internal.constraints.DatadogDataConstraints -import com.datadog.android.core.internal.domain.Serializer -import com.datadog.android.core.internal.utils.toJsonElement -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.internal.domain.model.ActionEvent -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.model.ResourceEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.google.gson.JsonElement -import com.google.gson.JsonObject - -internal class RumEventSerializer( - private val dataConstraints: DataConstraints = DatadogDataConstraints() -) : - Serializer { - - // region Serializer - - override fun serialize(model: RumEvent): String { - val json = model.event.toJson().asJsonObject - - addCustomAttributes( - dataConstraints.validateAttributes( - model.globalAttributes, - keyPrefix = GLOBAL_ATTRIBUTE_PREFIX - ), - json, - GLOBAL_ATTRIBUTE_PREFIX, - knownAttributes - ) - addCustomAttributes( - dataConstraints.validateAttributes( - model.userExtraAttributes, - keyPrefix = USER_ATTRIBUTE_PREFIX, - attributesGroupName = USER_EXTRA_GROUP_VERBOSE_NAME - ), - json, - USER_ATTRIBUTE_PREFIX - ) - model.customTimings?.let { - addCustomAttributes( - dataConstraints.validateAttributes( - it, - keyPrefix = VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX, - attributesGroupName = CUSTOM_TIMINGS_GROUP_VERBOSE_NAME - ), - json, - VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX - ) - } - - return json.toString() - } - - // endregion - - // region Internal - - private fun addCustomAttributes( - attributes: Map, - jsonEvent: JsonObject, - keyPrefix: String, - knownAttributesKeys: Set = emptySet() - ) { - attributes.forEach { - val rawKey = it.key - val key = - if (rawKey in knownAttributesKeys) rawKey else "$keyPrefix.$rawKey" - val value = it.value - jsonEvent.add(key, value.toJsonElement()) - } - } - - // endregion - - companion object { - internal val knownAttributes = setOf( - RumAttributes.ACTION_GESTURE_DIRECTION, - RumAttributes.ACTION_TARGET_PARENT_RESOURCE_ID, - RumAttributes.ACTION_TARGET_PARENT_CLASSNAME, - RumAttributes.ACTION_TARGET_PARENT_INDEX, - RumAttributes.ACTION_TARGET_CLASS_NAME, - RumAttributes.ACTION_TARGET_RESOURCE_ID, - RumAttributes.ACTION_TARGET_TITLE, - RumAttributes.ERROR_RESOURCE_METHOD, - RumAttributes.ERROR_RESOURCE_STATUS_CODE, - RumAttributes.ERROR_RESOURCE_URL - ) - - internal const val GLOBAL_ATTRIBUTE_PREFIX: String = "context" - internal const val USER_ATTRIBUTE_PREFIX: String = "$GLOBAL_ATTRIBUTE_PREFIX.usr" - internal const val VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX: String = "view.custom_timings" - internal const val USER_EXTRA_GROUP_VERBOSE_NAME = "user extra information" - internal const val CUSTOM_TIMINGS_GROUP_VERBOSE_NAME = "view custom timings" - } -} - -private fun Any.toJson(): JsonElement { - return when (this) { - is ViewEvent -> toJson() - is ActionEvent -> toJson() - is ResourceEvent -> toJson() - is ErrorEvent -> toJson() - else -> JsonObject() - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt deleted file mode 100644 index 4104af56bd..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt +++ /dev/null @@ -1,230 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.model.ActionEvent -import java.lang.ref.WeakReference -import java.util.UUID -import java.util.concurrent.TimeUnit -import kotlin.math.max - -internal class RumActionScope( - val parentScope: RumScope, - val waitForStop: Boolean, - eventTime: Time, - initialType: RumActionType, - initialName: String, - initialAttributes: Map -) : RumScope { - - private val eventTimestamp = eventTime.timestamp - internal val actionId: String = UUID.randomUUID().toString() - private var type: RumActionType = initialType - internal var name: String = initialName - private val startedNanos: Long = eventTime.nanoTime - private var lastInteractionNanos: Long = startedNanos - - internal val attributes: MutableMap = initialAttributes.toMutableMap() - - private val ongoingResourceKeys = mutableListOf>() - - internal var resourceCount: Long = 0 - internal var errorCount: Long = 0 - internal var crashCount: Long = 0 - internal var viewTreeChangeCount: Int = 0 - - private var sent = false - private var stopped = false - - // endregion - - override fun handleEvent(event: RumRawEvent, writer: Writer): RumScope? { - val now = event.eventTime.nanoTime - val isInactive = now - lastInteractionNanos > ACTION_INACTIVITY_NS - val isLongDuration = now - startedNanos > ACTION_MAX_DURATION_NS - ongoingResourceKeys.removeAll { it.get() == null } - val isOngoing = waitForStop && !stopped - val shouldStop = isInactive && ongoingResourceKeys.isEmpty() && !isOngoing - - when { - shouldStop -> sendAction(lastInteractionNanos, writer) - isLongDuration -> sendAction(now, writer) - event is RumRawEvent.ViewTreeChanged -> onViewTreeChanged(now) - event is RumRawEvent.StopView -> onStopView(now, writer) - event is RumRawEvent.StopAction -> onStopAction(event, now) - event is RumRawEvent.StartResource -> onStartResource(event, now) - event is RumRawEvent.StopResource -> onStopResource(event, now) - event is RumRawEvent.AddError -> onError(event, now, writer) - event is RumRawEvent.StopResourceWithError -> onResourceError(event, now) - } - - return if (sent) null else this - } - - override fun getRumContext(): RumContext { - return parentScope.getRumContext() - } - - // endregion - - // region Internal - - private fun onViewTreeChanged(now: Long) { - lastInteractionNanos = now - viewTreeChangeCount++ - } - - private fun onStopView( - now: Long, - writer: Writer - ) { - ongoingResourceKeys.clear() - sendAction(now, writer) - } - - private fun onStopAction( - event: RumRawEvent.StopAction, - now: Long - ) { - type = event.type - name = event.name - attributes.putAll(event.attributes) - stopped = true - lastInteractionNanos = now - } - - private fun onStartResource( - event: RumRawEvent.StartResource, - now: Long - ) { - lastInteractionNanos = now - resourceCount++ - ongoingResourceKeys.add(WeakReference(event.key)) - } - - private fun onStopResource( - event: RumRawEvent.StopResource, - now: Long - ) { - val keyRef = ongoingResourceKeys.firstOrNull { it.get() == event.key } - if (keyRef != null) { - ongoingResourceKeys.remove(keyRef) - lastInteractionNanos = now - } - } - - private fun onError( - event: RumRawEvent.AddError, - now: Long, - writer: Writer - ) { - lastInteractionNanos = now - errorCount++ - - if (event.isFatal) { - crashCount++ - - sendAction(now, writer) - } - } - - private fun onResourceError(event: RumRawEvent.StopResourceWithError, now: Long) { - val keyRef = ongoingResourceKeys.firstOrNull { it.get() == event.key } - if (keyRef != null) { - ongoingResourceKeys.remove(keyRef) - lastInteractionNanos = now - resourceCount-- - errorCount++ - } - } - - private fun sendAction( - endNanos: Long, - writer: Writer - ) { - if (sent) return - - if (resourceCount > 0 || errorCount > 0 || viewTreeChangeCount > 0) { - attributes.putAll(GlobalRum.globalAttributes) - - val context = getRumContext() - val user = RumFeature.userInfoProvider.getUserInfo() - - val actionEvent = ActionEvent( - date = eventTimestamp, - action = ActionEvent.Action( - type = type.toSchemaType(), - id = actionId, - target = ActionEvent.Target(name), - error = ActionEvent.Error(errorCount), - crash = ActionEvent.Crash(crashCount), - resource = ActionEvent.Resource(resourceCount), - loadingTime = max(endNanos - startedNanos, 1L) - ), - view = ActionEvent.View( - id = context.viewId.orEmpty(), - url = context.viewUrl.orEmpty() - ), - application = ActionEvent.Application(context.applicationId), - session = ActionEvent.Session( - id = context.sessionId, - type = ActionEvent.Type.USER - ), - usr = ActionEvent.Usr( - id = user.id, - name = user.name, - email = user.email - ), - dd = ActionEvent.Dd() - ) - val rumEvent = RumEvent( - event = actionEvent, - globalAttributes = attributes, - userExtraAttributes = user.extraInfo - ) - writer.write(rumEvent) - parentScope.handleEvent(RumRawEvent.SentAction(), writer) - } else { - devLogger.i( - "RUM Action $actionId ($type on $name) was dropped " + - "(no side effect was registered during its scope)" - ) - } - sent = true - } - - // endregion - - companion object { - internal const val ACTION_INACTIVITY_MS = 100L - internal const val ACTION_MAX_DURATION_MS = 5000L - private val ACTION_INACTIVITY_NS = TimeUnit.MILLISECONDS.toNanos(ACTION_INACTIVITY_MS) - internal val ACTION_MAX_DURATION_NS = TimeUnit.MILLISECONDS.toNanos(ACTION_MAX_DURATION_MS) - - fun fromEvent( - parentScope: RumScope, - event: RumRawEvent.StartAction - ): RumScope { - return RumActionScope( - parentScope, - event.waitForStop, - event.eventTime, - event.type, - event.name, - event.attributes - ) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt deleted file mode 100644 index 065f7a6455..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt +++ /dev/null @@ -1,39 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import java.util.UUID - -internal class RumApplicationScope( - applicationId: UUID, - internal val samplingRate: Float, - private val firstPartyHostDetector: FirstPartyHostDetector -) : RumScope { - - private val rumContext = RumContext(applicationId = applicationId.toString()) - internal val childScope: RumScope = RumSessionScope(this, samplingRate, firstPartyHostDetector) - - // region RumScope - - override fun handleEvent( - event: RumRawEvent, - writer: Writer - ): RumScope? { - childScope.handleEvent(event, writer) - return this - } - - override fun getRumContext(): RumContext { - return rumContext - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExt.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExt.kt deleted file mode 100644 index bf3d016807..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExt.kt +++ /dev/null @@ -1,176 +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 2016-Present Datadog, Inc. - */ -@file:Suppress("TooManyFunctions") - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.utils.sdkLogger -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.model.ActionEvent -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.model.ResourceEvent -import java.util.Locale - -internal fun String.toMethod(): ResourceEvent.Method { - return try { - ResourceEvent.Method.valueOf(this.toUpperCase(Locale.US)) - } catch (e: IllegalArgumentException) { - sdkLogger.w("Unable to convert $this to a valid method", e) - ResourceEvent.Method.GET - } -} - -internal fun String.toErrorMethod(): ErrorEvent.Method { - return try { - ErrorEvent.Method.valueOf(this.toUpperCase(Locale.US)) - } catch (e: IllegalArgumentException) { - sdkLogger.w("Unable to convert $this to a valid method", e) - ErrorEvent.Method.GET - } -} - -internal fun RumResourceKind.toSchemaType(): ResourceEvent.Type1 { - return when (this) { - RumResourceKind.BEACON -> ResourceEvent.Type1.BEACON - RumResourceKind.FETCH -> ResourceEvent.Type1.FETCH - RumResourceKind.XHR -> ResourceEvent.Type1.XHR - RumResourceKind.DOCUMENT -> ResourceEvent.Type1.DOCUMENT - RumResourceKind.IMAGE -> ResourceEvent.Type1.IMAGE - RumResourceKind.JS -> ResourceEvent.Type1.JS - RumResourceKind.FONT -> ResourceEvent.Type1.FONT - RumResourceKind.CSS -> ResourceEvent.Type1.CSS - RumResourceKind.MEDIA -> ResourceEvent.Type1.MEDIA - RumResourceKind.UNKNOWN, - RumResourceKind.OTHER -> ResourceEvent.Type1.OTHER - } -} - -internal fun RumErrorSource.toSchemaSource(): ErrorEvent.Source { - return when (this) { - RumErrorSource.NETWORK -> ErrorEvent.Source.NETWORK - RumErrorSource.SOURCE -> ErrorEvent.Source.SOURCE - RumErrorSource.CONSOLE -> ErrorEvent.Source.CONSOLE - RumErrorSource.LOGGER -> ErrorEvent.Source.LOGGER - RumErrorSource.AGENT -> ErrorEvent.Source.AGENT - RumErrorSource.WEBVIEW -> ErrorEvent.Source.WEBVIEW - } -} - -internal fun ResourceTiming.dns(): ResourceEvent.Dns? { - return if (dnsStart > 0) { - ResourceEvent.Dns(duration = dnsDuration, start = dnsStart) - } else null -} - -internal fun ResourceTiming.connect(): ResourceEvent.Connect? { - return if (connectStart > 0) { - ResourceEvent.Connect(duration = connectDuration, start = connectStart) - } else null -} - -internal fun ResourceTiming.ssl(): ResourceEvent.Ssl? { - return if (sslStart > 0) { - ResourceEvent.Ssl(duration = sslDuration, start = sslStart) - } else null -} - -internal fun ResourceTiming.firstByte(): ResourceEvent.FirstByte? { - return if (firstByteStart > 0) { - ResourceEvent.FirstByte(duration = firstByteDuration, start = firstByteStart) - } else null -} - -internal fun ResourceTiming.download(): ResourceEvent.Download? { - return if (downloadStart > 0) { - ResourceEvent.Download(duration = downloadDuration, start = downloadStart) - } else null -} - -internal fun RumActionType.toSchemaType(): ActionEvent.Type1 { - return when (this) { - RumActionType.TAP -> ActionEvent.Type1.TAP - RumActionType.SCROLL -> ActionEvent.Type1.SCROLL - RumActionType.SWIPE -> ActionEvent.Type1.SWIPE - RumActionType.CLICK -> ActionEvent.Type1.CLICK - RumActionType.CUSTOM -> ActionEvent.Type1.CUSTOM - } -} - -internal fun NetworkInfo.toResourceConnectivity(): ResourceEvent.Connectivity { - val status = if (isConnected()) { - ResourceEvent.Status.CONNECTED - } else { - ResourceEvent.Status.NOT_CONNECTED - } - val interfaces = when (connectivity) { - NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ResourceEvent.Interface.ETHERNET) - NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ResourceEvent.Interface.WIFI) - NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ResourceEvent.Interface.WIMAX) - NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ResourceEvent.Interface.BLUETOOTH) - NetworkInfo.Connectivity.NETWORK_2G, - NetworkInfo.Connectivity.NETWORK_3G, - NetworkInfo.Connectivity.NETWORK_4G, - NetworkInfo.Connectivity.NETWORK_5G, - NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, - NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ResourceEvent.Interface.CELLULAR) - NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ResourceEvent.Interface.OTHER) - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() - } - - val cellular = if (cellularTechnology != null || carrierName != null) { - ResourceEvent.Cellular( - technology = cellularTechnology, - carrierName = carrierName - ) - } else { - null - } - return ResourceEvent.Connectivity( - status, - interfaces, - cellular - ) -} - -internal fun NetworkInfo.toErrorConnectivity(): ErrorEvent.Connectivity { - val status = if (isConnected()) { - ErrorEvent.Status.CONNECTED - } else { - ErrorEvent.Status.NOT_CONNECTED - } - val interfaces = when (connectivity) { - NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ErrorEvent.Interface.ETHERNET) - NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ErrorEvent.Interface.WIFI) - NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ErrorEvent.Interface.WIMAX) - NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ErrorEvent.Interface.BLUETOOTH) - NetworkInfo.Connectivity.NETWORK_2G, - NetworkInfo.Connectivity.NETWORK_3G, - NetworkInfo.Connectivity.NETWORK_4G, - NetworkInfo.Connectivity.NETWORK_5G, - NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, - NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ErrorEvent.Interface.CELLULAR) - NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ErrorEvent.Interface.OTHER) - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() - } - - val cellular = if (cellularTechnology != null || carrierName != null) { - ErrorEvent.Cellular( - technology = cellularTechnology, - carrierName = carrierName - ) - } else { - null - } - return ErrorEvent.Connectivity( - status, - interfaces, - cellular - ) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt deleted file mode 100644 index f4813b0148..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt +++ /dev/null @@ -1,135 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.model.ViewEvent - -internal sealed class RumRawEvent { - - abstract val eventTime: Time - - internal data class StartView( - val key: Any, - val name: String, - val attributes: Map, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class StopView( - val key: Any, - val attributes: Map, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class StartAction( - val type: RumActionType, - val name: String, - val waitForStop: Boolean, - val attributes: Map, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class StopAction( - val type: RumActionType, - val name: String, - val attributes: Map, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class StartResource( - val key: String, - val url: String, - val method: String, - val attributes: Map, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class WaitForResourceTiming( - val key: String, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class AddResourceTiming( - val key: String, - val timing: ResourceTiming, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class StopResource( - val key: String, - val statusCode: Long?, - val size: Long?, - val kind: RumResourceKind, - val attributes: Map, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class StopResourceWithError( - val key: String, - val statusCode: Long?, - val message: String, - val source: RumErrorSource, - val throwable: Throwable, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class AddError( - val message: String, - val source: RumErrorSource, - val throwable: Throwable?, - val stacktrace: String?, - val isFatal: Boolean, - val attributes: Map, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class UpdateViewLoadingTime( - val key: Any, - val loadingTime: Long, - val loadingType: ViewEvent.LoadingType, - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class SentResource( - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class SentAction( - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class SentError( - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal class ViewTreeChanged( - override val eventTime: Time - ) : RumRawEvent() - - internal data class ResetSession( - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class KeepAlive( - override val eventTime: Time = Time() - ) : RumRawEvent() - - internal data class ApplicationStarted( - override val eventTime: Time, - val applicationStartupNanos: Long - ) : RumRawEvent() - - internal data class AddCustomTiming( - val name: String, - override val eventTime: Time = Time() - ) : RumRawEvent() -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt deleted file mode 100644 index 05c09a0c97..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt +++ /dev/null @@ -1,252 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.model.ResourceEvent -import java.util.UUID - -internal class RumResourceScope( - internal val parentScope: RumScope, - internal val url: String, - internal val method: String, - internal val key: String, - eventTime: Time, - initialAttributes: Map, - internal val firstPartyHostDetector: FirstPartyHostDetector -) : RumScope { - - internal val resourceId: String = UUID.randomUUID().toString() - internal val attributes: MutableMap = initialAttributes.toMutableMap() - private var timing: ResourceTiming? = null - private val initialContext = parentScope.getRumContext() - - private val eventTimestamp = eventTime.timestamp - private val startedNanos: Long = eventTime.nanoTime - private val networkInfo = RumFeature.networkInfoProvider.getLatestNetworkInfo() - - private var sent = false - private var waitForTiming = false - private var stopped = false - private var kind: RumResourceKind = RumResourceKind.UNKNOWN - private var statusCode: Long? = null - private var size: Long? = null - - // region RumScope - - override fun handleEvent(event: RumRawEvent, writer: Writer): RumScope? { - when (event) { - is RumRawEvent.WaitForResourceTiming -> if (key == event.key) waitForTiming = true - is RumRawEvent.AddResourceTiming -> onAddResourceTiming(event, writer) - is RumRawEvent.StopResource -> onStopResource(event, writer) - is RumRawEvent.StopResourceWithError -> onStopResourceWithError(event, writer) - } - - return if (sent) null else this - } - - override fun getRumContext(): RumContext { - return initialContext - } - - // endregion - - // region Internal - - private fun onStopResource( - event: RumRawEvent.StopResource, - writer: Writer - ) { - if (key != event.key) return - - stopped = true - attributes.putAll(event.attributes) - kind = event.kind - statusCode = event.statusCode - size = event.size - - if (!(waitForTiming && timing == null)) { - sendResource(kind, event.statusCode, event.size, event.eventTime, writer) - } - } - - private fun onAddResourceTiming( - event: RumRawEvent.AddResourceTiming, - writer: Writer - ) { - if (key != event.key) return - - timing = event.timing - if (stopped && !sent) { - sendResource(kind, statusCode, size, event.eventTime, writer) - } - } - - private fun onStopResourceWithError( - event: RumRawEvent.StopResourceWithError, - writer: Writer - ) { - if (key != event.key) return - - sendError( - event.message, - event.source, - event.statusCode, - event.throwable, - writer - ) - } - - @Suppress("LongMethod") - private fun sendResource( - kind: RumResourceKind, - statusCode: Long?, - size: Long?, - eventTime: Time, - writer: Writer - ) { - attributes.putAll(GlobalRum.globalAttributes) - val traceId = attributes.remove(RumAttributes.TRACE_ID)?.toString() - val spanId = attributes.remove(RumAttributes.SPAN_ID)?.toString() - - val context = getRumContext() - val user = RumFeature.userInfoProvider.getUserInfo() - - val finalTiming = timing - val duration = eventTime.nanoTime - startedNanos - val isFirstParty = firstPartyHostDetector.isFirstPartyUrl(url) - - val resourceEvent = ResourceEvent( - date = eventTimestamp, - resource = ResourceEvent.Resource( - id = resourceId, - type = kind.toSchemaType(), - url = url, - duration = duration, - method = method.toMethod(), - statusCode = statusCode, - size = size, - dns = finalTiming?.dns(), - connect = finalTiming?.connect(), - ssl = finalTiming?.ssl(), - firstByte = finalTiming?.firstByte(), - download = finalTiming?.download(), - firstParty = if (isFirstParty) true else null - ), - action = context.actionId?.let { ResourceEvent.Action(it) }, - view = ResourceEvent.View( - id = context.viewId.orEmpty(), - url = context.viewUrl.orEmpty() - ), - usr = ResourceEvent.Usr( - id = user.id, - name = user.name, - email = user.email - ), - connectivity = networkInfo.toResourceConnectivity(), - application = ResourceEvent.Application(context.applicationId), - session = ResourceEvent.Session( - id = context.sessionId, - type = ResourceEvent.Type.USER - ), - dd = ResourceEvent.Dd( - traceId = traceId, - spanId = spanId - ) - ) - val rumEvent = RumEvent( - event = resourceEvent, - globalAttributes = attributes, - userExtraAttributes = user.extraInfo - ) - writer.write(rumEvent) - parentScope.handleEvent(RumRawEvent.SentResource(), writer) - sent = true - } - - private fun sendError( - message: String, - source: RumErrorSource, - statusCode: Long?, - throwable: Throwable, - writer: Writer - ) { - attributes.putAll(GlobalRum.globalAttributes) - - val context = getRumContext() - val user = RumFeature.userInfoProvider.getUserInfo() - - val errorEvent = ErrorEvent( - date = eventTimestamp, - error = ErrorEvent.Error( - message = message, - source = source.toSchemaSource(), - stack = throwable.loggableStackTrace(), - isCrash = false, - resource = ErrorEvent.Resource( - url = url, - method = method.toErrorMethod(), - statusCode = statusCode ?: 0 - ) - ), - action = context.actionId?.let { ErrorEvent.Action(it) }, - view = ErrorEvent.View( - id = context.viewId.orEmpty(), - url = context.viewUrl.orEmpty() - ), - usr = ErrorEvent.Usr( - id = user.id, - name = user.name, - email = user.email - ), - connectivity = networkInfo.toErrorConnectivity(), - application = ErrorEvent.Application(context.applicationId), - session = ErrorEvent.Session(id = context.sessionId, type = ErrorEvent.Type.USER), - dd = ErrorEvent.Dd() - ) - val rumEvent = RumEvent( - event = errorEvent, - globalAttributes = attributes, - userExtraAttributes = user.extraInfo - ) - writer.write(rumEvent) - parentScope.handleEvent(RumRawEvent.SentError(), writer) - sent = true - } - - // endregion - - companion object { - fun fromEvent( - parentScope: RumScope, - event: RumRawEvent.StartResource, - firstPartyHostDetector: FirstPartyHostDetector - ): RumScope { - return RumResourceScope( - parentScope, - event.url, - event.method, - event.key, - event.eventTime, - event.attributes, - firstPartyHostDetector - ) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumScope.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumScope.kt deleted file mode 100644 index 3f964c0c0a..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumScope.kt +++ /dev/null @@ -1,29 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface RumScope { - - /** - * Handles an incoming event. - * If needed, writes a RumEvent to the provided writer. - * @return this instance if this scope is still valid, or null if it no longer can process - * events - */ - fun handleEvent( - event: RumRawEvent, - writer: Writer - ): RumScope? - - fun getRumContext(): RumContext -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt deleted file mode 100644 index 354263701f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt +++ /dev/null @@ -1,143 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.Datadog -import com.datadog.android.core.internal.data.NoOpWriter -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import java.security.SecureRandom -import java.util.UUID -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicLong - -internal class RumSessionScope( - private val parentScope: RumScope, - internal val samplingRate: Float, - internal val firstPartyHostDetector: FirstPartyHostDetector, - private val sessionInactivityNanos: Long = DEFAULT_SESSION_INACTIVITY_NS, - private val sessionMaxDurationNanos: Long = DEFAULT_SESSION_MAX_DURATION_NS -) : RumScope { - - internal val activeChildrenScopes = mutableListOf() - - internal var keepSession: Boolean = false - internal var sessionId = RumContext.NULL_UUID - internal val sessionStartNs = AtomicLong(System.nanoTime()) - internal val lastUserInteractionNs = AtomicLong(0L) - - private var resetSessionTime: Long? = null - - private var applicationDisplayed: Boolean = false - - private val random = SecureRandom() - private val noOpWriter = NoOpWriter() - - init { - GlobalRum.updateRumContext(getRumContext()) - } - - // region RumScope - - override fun handleEvent( - event: RumRawEvent, - writer: Writer - ): RumScope? { - if (event is RumRawEvent.ResetSession) { - sessionId = RumContext.NULL_UUID - resetSessionTime = System.nanoTime() - applicationDisplayed = false - } - updateSessionIdIfNeeded() - - val actualWriter = if (keepSession) writer else noOpWriter - - val activeChildrenCount = activeChildrenScopes.size - val iterator = activeChildrenScopes.iterator() - while (iterator.hasNext()) { - val scope = iterator.next().handleEvent(event, actualWriter) - if (scope == null) { - iterator.remove() - } - } - - if (event is RumRawEvent.StartView) { - val viewScope = RumViewScope.fromEvent(this, event, firstPartyHostDetector) - - onApplicationDisplayed(event, viewScope, actualWriter) - activeChildrenScopes.add(viewScope) - } else if (activeChildrenCount == 0) { - devLogger.w(MESSAGE_MISSING_VIEW) - } - - return this - } - - override fun getRumContext(): RumContext { - updateSessionIdIfNeeded() - return if (keepSession) { - parentScope.getRumContext().copy(sessionId = sessionId) - } else { - RumContext() - } - } - - // endregion - - // region Internal - - internal fun onApplicationDisplayed( - event: RumRawEvent.StartView, - viewScope: RumViewScope, - writer: Writer - ) { - if (!applicationDisplayed) { - applicationDisplayed = true - val applicationStartTime = resetSessionTime ?: Datadog.startupTimeNs - viewScope.handleEvent( - RumRawEvent.ApplicationStarted(event.eventTime, applicationStartTime), - writer - ) - } - } - - @Synchronized - private fun updateSessionIdIfNeeded() { - val nanoTime = System.nanoTime() - val isNewSession = sessionId == RumContext.NULL_UUID - val sessionLength = nanoTime - sessionStartNs.get() - val duration = nanoTime - lastUserInteractionNs.get() - val isInactiveSession = duration >= sessionInactivityNanos - val isLongSession = sessionLength >= sessionMaxDurationNanos - - if (isNewSession || isInactiveSession || isLongSession) { - keepSession = (random.nextFloat() * 100f) < samplingRate - sessionStartNs.set(nanoTime) - sessionId = UUID.randomUUID().toString() - } - - lastUserInteractionNs.set(nanoTime) - } - - // endregion - - companion object { - internal val DEFAULT_SESSION_INACTIVITY_NS = TimeUnit.MINUTES.toNanos(15) - internal val DEFAULT_SESSION_MAX_DURATION_NS = TimeUnit.HOURS.toNanos(4) - - internal const val MESSAGE_MISSING_VIEW = - "A RUM event was detected, but no view is active. " + - "To track views automatically, try calling the " + - "DatadogConfig.Builder.useViewTrackingStrategy() method.\n" + - "You can also track views manually using the RumMonitor.startView() and " + - "RumMonitor.stopView() methods." - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt deleted file mode 100644 index b68861d551..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt +++ /dev/null @@ -1,399 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.model.ActionEvent -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import java.lang.ref.Reference -import java.lang.ref.WeakReference -import java.util.UUID -import kotlin.math.max - -internal class RumViewScope( - private val parentScope: RumScope, - key: Any, - internal val name: String, - eventTime: Time, - initialAttributes: Map, - internal val firstPartyHostDetector: FirstPartyHostDetector -) : RumScope { - - internal val urlName = name.replace('.', '/') - - internal val keyRef: Reference = WeakReference(key) - internal val attributes: MutableMap = initialAttributes.toMutableMap() - - private var sessionId: String = parentScope.getRumContext().sessionId - internal var viewId: String = UUID.randomUUID().toString() - private set - private val startedNanos: Long = eventTime.nanoTime - - internal val eventTimestamp = eventTime.timestamp - - internal var activeActionScope: RumScope? = null - internal val activeResourceScopes = mutableMapOf() - - private var resourceCount: Long = 0 - private var actionCount: Long = 0 - private var errorCount: Long = 0 - private var crashCount: Long = 0 - private var version: Long = 1 - private var loadingTime: Long? = null - private var loadingType: ViewEvent.LoadingType? = null - private val customTimings: MutableMap = mutableMapOf() - - internal var stopped: Boolean = false - - init { - GlobalRum.updateRumContext(getRumContext()) - attributes.putAll(GlobalRum.globalAttributes) - } - - // region RumScope - - override fun handleEvent( - event: RumRawEvent, - writer: Writer - ): RumScope? { - - when (event) { - is RumRawEvent.SentError -> { - errorCount++ - sendViewUpdate(event, writer) - } - is RumRawEvent.SentResource -> { - resourceCount++ - sendViewUpdate(event, writer) - } - is RumRawEvent.SentAction -> { - actionCount++ - sendViewUpdate(event, writer) - } - is RumRawEvent.StartView -> onStartView(event, writer) - is RumRawEvent.StopView -> onStopView(event, writer) - is RumRawEvent.StartAction -> onStartAction(event, writer) - is RumRawEvent.StartResource -> onStartResource(event, writer) - is RumRawEvent.AddError -> onAddError(event, writer) - is RumRawEvent.KeepAlive -> onKeepAlive(event, writer) - is RumRawEvent.UpdateViewLoadingTime -> onUpdateViewLoadingTime(event, writer) - is RumRawEvent.ApplicationStarted -> onApplicationStarted(event, writer) - is RumRawEvent.AddCustomTiming -> onAddCustomTiming(event, writer) - else -> delegateEventToChildren(event, writer) - } - - return if (stopped && activeResourceScopes.isEmpty()) { - null - } else { - this - } - } - - override fun getRumContext(): RumContext { - val parentContext = parentScope.getRumContext() - if (parentContext.sessionId != sessionId) { - sessionId = parentContext.sessionId - viewId = UUID.randomUUID().toString() - } - - return parentContext - .copy( - viewId = viewId, - viewUrl = urlName, - actionId = (activeActionScope as? RumActionScope)?.actionId - ) - } - - // endregion - - // region Internal - - private fun onStartView( - event: RumRawEvent.StartView, - writer: Writer - ) { - if (!stopped) { - sendViewUpdate(event, writer) - delegateEventToChildren(event, writer) - stopped = true - } - } - - private fun onStopView( - event: RumRawEvent.StopView, - writer: Writer - ) { - delegateEventToChildren(event, writer) - val startedKey = keyRef.get() - val shouldStop = (event.key == startedKey) || (startedKey == null) - if (shouldStop && !stopped) { - attributes.putAll(event.attributes) - sendViewUpdate(event, writer) - stopped = true - } - } - - private fun onStartAction( - event: RumRawEvent.StartAction, - writer: Writer - ) { - delegateEventToChildren(event, writer) - - if (stopped || activeActionScope != null) return - - activeActionScope = RumActionScope.fromEvent(this, event) - } - - private fun onStartResource( - event: RumRawEvent.StartResource, - writer: Writer - ) { - delegateEventToChildren(event, writer) - if (stopped) return - - val updatedEvent = event.copy( - attributes = addExtraAttributes(event.attributes) - ) - activeResourceScopes[event.key] = RumResourceScope.fromEvent( - this, - updatedEvent, - firstPartyHostDetector - ) - } - - private fun onAddError( - event: RumRawEvent.AddError, - writer: Writer - ) { - delegateEventToChildren(event, writer) - if (stopped) return - - val context = getRumContext() - val user = RumFeature.userInfoProvider.getUserInfo() - val updatedAttributes = addExtraAttributes(event.attributes) - val networkInfo = RumFeature.networkInfoProvider.getLatestNetworkInfo() - - val errorEvent = ErrorEvent( - date = eventTimestamp, - error = ErrorEvent.Error( - message = event.message, - source = event.source.toSchemaSource(), - stack = event.stacktrace ?: event.throwable?.loggableStackTrace(), - isCrash = event.isFatal - ), - action = context.actionId?.let { ErrorEvent.Action(it) }, - view = ErrorEvent.View( - id = context.viewId.orEmpty(), - url = context.viewUrl.orEmpty() - ), - usr = ErrorEvent.Usr( - id = user.id, - name = user.name, - email = user.email - ), - connectivity = networkInfo.toErrorConnectivity(), - application = ErrorEvent.Application(context.applicationId), - session = ErrorEvent.Session(id = context.sessionId, type = ErrorEvent.Type.USER), - dd = ErrorEvent.Dd() - ) - val rumEvent = RumEvent( - event = errorEvent, - globalAttributes = updatedAttributes, - userExtraAttributes = user.extraInfo - ) - writer.write(rumEvent) - errorCount++ - if (event.isFatal) { - crashCount++ - } - sendViewUpdate(event, writer) - } - - private fun onAddCustomTiming(event: RumRawEvent.AddCustomTiming, writer: Writer) { - customTimings[event.name] = max(event.eventTime.nanoTime - startedNanos, 1L) - sendViewUpdate(event, writer) - } - - private fun onKeepAlive( - event: RumRawEvent.KeepAlive, - writer: Writer - ) { - delegateEventToChildren(event, writer) - if (stopped) return - - sendViewUpdate(event, writer) - } - - private fun delegateEventToChildren( - event: RumRawEvent, - writer: Writer - ) { - delegateEventToResources(event, writer) - delegateEventToAction(event, writer) - } - - private fun delegateEventToAction( - event: RumRawEvent, - writer: Writer - ) { - val currentAction = activeActionScope - if (currentAction != null) { - val updatedAction = currentAction.handleEvent(event, writer) - if (updatedAction == null) { - activeActionScope = null - } - } - } - - private fun delegateEventToResources( - event: RumRawEvent, - writer: Writer - ) { - val iterator = activeResourceScopes.iterator() - while (iterator.hasNext()) { - val entry = iterator.next() - val scope = entry.value.handleEvent(event, writer) - if (scope == null) { - iterator.remove() - } - } - } - - private fun sendViewUpdate(event: RumRawEvent, writer: Writer) { - attributes.putAll(GlobalRum.globalAttributes) - version++ - val updatedDurationNs = event.eventTime.nanoTime - startedNanos - val context = getRumContext() - val user = RumFeature.userInfoProvider.getUserInfo() - - val viewEvent = ViewEvent( - date = eventTimestamp, - view = ViewEvent.View( - id = context.viewId.orEmpty(), - url = context.viewUrl.orEmpty(), - loadingTime = loadingTime, - loadingType = loadingType, - timeSpent = updatedDurationNs, - action = ViewEvent.Action(actionCount), - resource = ViewEvent.Resource(resourceCount), - error = ViewEvent.Error(errorCount), - crash = ViewEvent.Crash(crashCount) - ), - usr = ViewEvent.Usr( - id = user.id, - name = user.name, - email = user.email - ), - application = ViewEvent.Application(context.applicationId), - session = ViewEvent.Session(id = context.sessionId, type = ViewEvent.Type.USER), - dd = ViewEvent.Dd(documentVersion = version) - ) - - val rumEvent = RumEvent( - event = viewEvent, - globalAttributes = attributes, - userExtraAttributes = user.extraInfo, - customTimings = customTimings - ) - writer.write(rumEvent) - } - - private fun addExtraAttributes( - attributes: Map - ): MutableMap { - return attributes.toMutableMap() - .apply { putAll(GlobalRum.globalAttributes) } - } - - private fun onUpdateViewLoadingTime( - event: RumRawEvent.UpdateViewLoadingTime, - writer: Writer - ) { - val startedKey = keyRef.get() - if (event.key != startedKey) { - return - } - loadingTime = event.loadingTime - loadingType = event.loadingType - sendViewUpdate(event, writer) - } - - private fun onApplicationStarted( - event: RumRawEvent.ApplicationStarted, - writer: Writer - ) { - val context = getRumContext() - val user = RumFeature.userInfoProvider.getUserInfo() - - val actionEvent = ActionEvent( - date = eventTimestamp, - action = ActionEvent.Action( - type = ActionEvent.Type1.APPLICATION_START, - id = UUID.randomUUID().toString(), - loadingTime = getStartupTime(event) - ), - view = ActionEvent.View( - id = context.viewId.orEmpty(), - url = context.viewUrl.orEmpty() - ), - usr = ActionEvent.Usr( - id = user.id, - name = user.name, - email = user.email - ), - application = ActionEvent.Application(context.applicationId), - session = ActionEvent.Session( - id = context.sessionId, - type = ActionEvent.Type.USER - ), - dd = ActionEvent.Dd() - ) - val rumEvent = RumEvent( - event = actionEvent, - globalAttributes = GlobalRum.globalAttributes, - userExtraAttributes = user.extraInfo - ) - writer.write(rumEvent) - - actionCount++ - sendViewUpdate(event, writer) - } - - private fun getStartupTime(event: RumRawEvent.ApplicationStarted): Long { - val now = event.eventTime.nanoTime - val startupTime = event.applicationStartupNanos - return max(now - startupTime, 1L) - } - - // endregion - - companion object { - - internal fun fromEvent( - parentScope: RumScope, - event: RumRawEvent.StartView, - firstPartyHostDetector: FirstPartyHostDetector - ): RumViewScope { - return RumViewScope( - parentScope, - event.key, - event.name, - event.eventTime, - event.attributes, - firstPartyHostDetector - ) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategy.kt deleted file mode 100644 index fd5f4a7a6e..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategy.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation - -import android.app.Activity -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy -import com.datadog.android.rum.tracking.ActivityLifecycleTrackingStrategy - -internal class GesturesTrackingStrategy( - internal val gesturesTracker: GesturesTracker -) : - ActivityLifecycleTrackingStrategy(), - UserActionTrackingStrategy { - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - gesturesTracker.startTracking(activity.window, activity) - } - - override fun onActivityPaused(activity: Activity) { - super.onActivityPaused(activity) - gesturesTracker.stopTracking(activity.window, activity) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyApi29.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyApi29.kt deleted file mode 100644 index a43d171b28..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyApi29.kt +++ /dev/null @@ -1,24 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation - -import android.app.Activity -import android.os.Bundle -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy -import com.datadog.android.rum.tracking.ActivityLifecycleTrackingStrategy - -internal class GesturesTrackingStrategyApi29( - internal val gesturesTracker: GesturesTracker -) : ActivityLifecycleTrackingStrategy(), - UserActionTrackingStrategy { - - override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) { - gesturesTracker.startTracking(activity.window, activity) - super.onActivityPreCreated(activity, savedInstanceState) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker.kt deleted file mode 100644 index 8c8def5450..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker.kt +++ /dev/null @@ -1,65 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.content.Context -import android.view.Window -import com.datadog.android.rum.tracking.ViewAttributesProvider -import java.lang.ref.WeakReference - -internal class DatadogGesturesTracker( - internal val targetAttributesProviders: Array -) : GesturesTracker { - - // region GesturesTracker - - override fun startTracking(window: Window?, context: Context) { - @Suppress("SENSELESS_COMPARISON") - if (window == null) { - return - } - - val toWrap = window.callback ?: NoOpWindowCallback() - val gesturesDetector = generateGestureDetector(context, window) - - window.callback = WindowCallbackWrapper(toWrap, gesturesDetector) - } - - override fun stopTracking(window: Window?, context: Context) { - if (window == null) { - return - } - - val currentCallback = window.callback - if (currentCallback is WindowCallbackWrapper) { - if (currentCallback.wrappedCallback !is NoOpWindowCallback) { - window.callback = currentCallback.wrappedCallback - } else { - window.callback = null - } - } - } - - // endregion - - // region Internal - - internal fun generateGestureDetector( - context: Context, - window: Window - ): GesturesDetectorWrapper { - return GesturesDetectorWrapper( - context, - GesturesListener( - WeakReference(window), - targetAttributesProviders - ) - ) - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt deleted file mode 100644 index c11add07c9..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt +++ /dev/null @@ -1,301 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.view.Window -import android.widget.AbsListView -import androidx.core.view.ScrollingView -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.internal.monitor.DatadogRumMonitor -import com.datadog.android.rum.tracking.ViewAttributesProvider -import java.lang.ref.WeakReference -import java.util.LinkedList -import kotlin.math.abs - -internal class GesturesListener( - private val windowReference: WeakReference, - private val attributesProviders: Array = emptyArray() -) : GestureDetector.OnGestureListener { - - private val coordinatesContainer = IntArray(2) - private var scrollEventType: RumActionType? = null - private var gestureDirection = "" - private var scrollTargetReference: WeakReference = WeakReference(null) - private var onTouchDownXPos = 0f - private var onTouchDownYPos = 0f - - // region GesturesListener - - override fun onShowPress(e: MotionEvent) { - // No Op - } - - override fun onSingleTapUp(e: MotionEvent): Boolean { - val decorView = windowReference.get()?.decorView - handleTapUp(decorView, e) - return false - } - - override fun onDown(e: MotionEvent): Boolean { - resetScrollEventParameters() - onTouchDownXPos = e.x - onTouchDownYPos = e.y - return false - } - - fun onUp(event: MotionEvent) { - val decorView = windowReference.get()?.decorView - closeScrollOrSwipeEventIfAny(decorView, event) - resetScrollEventParameters() - } - - override fun onFling( - startDownEvent: MotionEvent, - endUpEvent: MotionEvent, - velocityX: Float, - velocityY: Float - ): Boolean { - scrollEventType = RumActionType.SWIPE - return false - } - - override fun onScroll( - startDownEvent: MotionEvent, - currentMoveEvent: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - val rumMonitor = GlobalRum.get() - val decorView = windowReference.get()?.decorView - if (decorView == null || rumMonitor !is DatadogRumMonitor) { - return false - } - - // we only start the user action once - if (scrollEventType == null) { - // check if we find a valid target - val scrollTarget = findTargetForScroll(decorView, startDownEvent.x, startDownEvent.y) - if (scrollTarget != null) { - scrollTargetReference = WeakReference(scrollTarget) - rumMonitor.startUserAction( - RumActionType.CUSTOM, - "", - emptyMap() - ) - } else { - return false - } - scrollEventType = RumActionType.SCROLL - } - - return false - } - - override fun onLongPress(e: MotionEvent) { - // No Op - } - - // endregion - - // region Internal - - private fun closeScrollOrSwipeEventIfAny(decorView: View?, onUpEvent: MotionEvent) { - val type = scrollEventType ?: return - - val registeredRumMonitor = GlobalRum.get() - val scrollTarget = scrollTargetReference.get() - if (decorView == null || - registeredRumMonitor !is DatadogRumMonitor || - scrollTarget == null - ) { - return - } - - val targetId: String = resourceIdName(scrollTarget.id) - val attributes = resolveAttributes(scrollTarget, targetId, onUpEvent) - registeredRumMonitor.stopUserAction( - type, - targetName(scrollTarget, targetId), - attributes - ) - } - - private fun resolveAttributes( - scrollTarget: View, - targetId: String, - onUpEvent: MotionEvent - ): MutableMap { - val attributes = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to targetId - ) - gestureDirection = resolveGestureDirection(onUpEvent) - attributes.put(RumAttributes.ACTION_GESTURE_DIRECTION, gestureDirection) - - attributesProviders.forEach { - it.extractAttributes(scrollTarget, attributes) - } - return attributes - } - - private fun resetScrollEventParameters() { - scrollTargetReference.clear() - scrollEventType = null - gestureDirection = "" - onTouchDownYPos = 0f - onTouchDownXPos = 0f - } - - private fun handleTapUp(decorView: View?, e: MotionEvent) { - if (decorView != null) { - findTargetForTap(decorView, e.x, e.y)?.let { target -> - val targetId: String = resourceIdName(target.id) - val attributes = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to target.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to targetId - ) - attributesProviders.forEach { - it.extractAttributes(target, attributes) - } - GlobalRum.get().addUserAction( - RumActionType.TAP, - targetName(target, targetId), - attributes - ) - } - } - } - - private fun findTargetForTap(decorView: View, x: Float, y: Float): View? { - val queue = LinkedList() - queue.addFirst(decorView) - var target: View? = null - - while (queue.isNotEmpty()) { - val view = queue.removeFirst() - - if (isValidTapTarget(view)) { - target = view - } - - if (view is ViewGroup) { - handleViewGroup(view, x, y, queue, coordinatesContainer) - } - } - - if (target == null) { - devLogger.i(MSG_NO_TARGET_TAP) - } - return target - } - - private fun findTargetForScroll(decorView: View, x: Float, y: Float): View? { - val queue = LinkedList() - queue.add(decorView) - - while (queue.isNotEmpty()) { - val view = queue.removeFirst() - - if (isValidScrollableTarget(view)) { - return view - } - - if (view is ViewGroup) { - handleViewGroup(view, x, y, queue, coordinatesContainer) - } - } - devLogger.i(MSG_NO_TARGET_SCROLL_SWIPE) - return null - } - - private fun handleViewGroup( - view: ViewGroup, - x: Float, - y: Float, - stack: LinkedList, - coordinatesContainer: IntArray - ) { - for (i in 0 until view.childCount) { - val child = view.getChildAt(i) - if (hitTest(child, x, y, coordinatesContainer)) { - stack.add(child) - } - } - } - - private fun isValidTapTarget(view: View): Boolean { - return view.isClickable && view.visibility == View.VISIBLE - } - - private fun isValidScrollableTarget(view: View): Boolean { - return view.visibility == View.VISIBLE && isScrollableView(view) - } - - private fun isScrollableView(view: View): Boolean { - return ScrollingView::class.java.isAssignableFrom(view.javaClass) || - AbsListView::class.java.isAssignableFrom(view.javaClass) - } - - private fun hitTest( - view: View, - x: Float, - y: Float, - container: IntArray - ): Boolean { - view.getLocationOnScreen(container) - val vx = container[0] - val vy = container[1] - val w = view.width - val h = view.height - - return !(x < vx || x > vx + w || y < vy || y > vy + h) - } - - private fun resolveGestureDirection(endEvent: MotionEvent): String { - val diffX = endEvent.x - onTouchDownXPos - val diffY = endEvent.y - onTouchDownYPos - return if (abs(diffX) > abs(diffY)) { - if (diffX > 0) { - SCROLL_DIRECTION_LEFT - } else { - SCROLL_DIRECTION_RIGHT - } - } else { - if (diffY > 0) { - SCROLL_DIRECTION_DOWN - } else { - SCROLL_DIRECTION_UP - } - } - } - - // endregion - - companion object { - - internal const val SCROLL_DIRECTION_LEFT = "left" - internal const val SCROLL_DIRECTION_RIGHT = "right" - internal const val SCROLL_DIRECTION_UP = "up" - internal const val SCROLL_DIRECTION_DOWN = "down" - - internal val MSG_NO_TARGET_TAP = "We could not find a valid target for " + - "the ${RumActionType.TAP.name} event." + - "The DecorView was empty and either transparent " + - "or not clickable for this Activity." - internal val MSG_NO_TARGET_SCROLL_SWIPE = "We could not find a valid target for " + - "the ${RumActionType.SCROLL.name} or ${RumActionType.SWIPE.name} event. " + - "The DecorView was empty and either transparent " + - "or not clickable for this Activity." - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesUtils.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesUtils.kt deleted file mode 100644 index 5c5312a187..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesUtils.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.content.res.Resources -import com.datadog.android.core.internal.CoreFeature - -internal fun targetName(target: Any, id: String): String { - return "${target.javaClass.simpleName}($id)" -} - -internal fun resourceIdName(id: Int): String { - @Suppress("SwallowedException") - return try { - CoreFeature.contextRef.get()?.resources?.getResourceEntryName(id) - ?: idAsStringHexa(id) - } catch (e: Resources.NotFoundException) { - idAsStringHexa(id) - } -} - -private fun idAsStringHexa(id: Int) = "0x${id.toString(16)}" diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/WindowCallbackWrapper.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/WindowCallbackWrapper.kt deleted file mode 100644 index 14d438e081..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/WindowCallbackWrapper.kt +++ /dev/null @@ -1,74 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.view.KeyEvent -import android.view.MenuItem -import android.view.MotionEvent -import android.view.Window -import com.datadog.android.core.internal.utils.sdkLogger -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumAttributes -import java.lang.Exception - -internal class WindowCallbackWrapper( - val wrappedCallback: Window.Callback, - val gesturesDetector: GesturesDetectorWrapper -) : Window.Callback by wrappedCallback { - - // region Window.Callback - - override fun dispatchTouchEvent(event: MotionEvent?): Boolean { - // we copy it and delegate it to the gesture detector for analysis - val copy = copyEvent(event) - @Suppress("TooGenericExceptionCaught") - try { - gesturesDetector.onTouchEvent(copy) - } catch (e: Exception) { - sdkLogger.e( - "$TAG: error while processing the MotionEvent", - e - ) - } finally { - sdkLogger.v("$TAG: Recycling the MotionEvent copy") - copy.recycle() - } - return wrappedCallback.dispatchTouchEvent(event) - } - - override fun onMenuItemSelected(featureId: Int, item: MenuItem): Boolean { - val resourceId = resourceIdName(item.itemId) - val attributes = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to item.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to resourceId, - RumAttributes.ACTION_TARGET_TITLE to item.title - ) - GlobalRum.get().addUserAction(RumActionType.TAP, targetName(item, resourceId), attributes) - return wrappedCallback.onMenuItemSelected(featureId, item) - } - - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - if (event?.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { - // TODO RUMM-495 add a BACK action to the json schema - GlobalRum.get().addUserAction(RumActionType.CUSTOM, "back", emptyMap()) - } - return wrappedCallback.dispatchKeyEvent(event) - } - - // endregion - - // region Internal - - internal fun copyEvent(event: MotionEvent?) = MotionEvent.obtain(event) - - // endregion - - companion object { - const val TAG = "WindowCallbackWrapper" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedRumMonitor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedRumMonitor.kt deleted file mode 100644 index 1c577d08ba..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedRumMonitor.kt +++ /dev/null @@ -1,41 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.monitor - -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.tools.annotation.NoOpImplementation - -@NoOpImplementation -internal interface AdvancedRumMonitor : RumMonitor { - - fun resetSession() - - fun viewTreeChanged(eventTime: Time) - - fun waitForResourceTiming(key: String) - - fun updateViewLoadingTime(key: Any, loadingTimeInNs: Long, type: ViewEvent.LoadingType) - - fun addResourceTiming(key: String, timing: ResourceTiming) - - fun addCrash( - message: String, - source: RumErrorSource, - throwable: Throwable - ) - - fun addErrorWithStacktrace( - message: String, - source: RumErrorSource, - stacktrace: String?, - attributes: Map - ) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt deleted file mode 100644 index 720fb8135c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt +++ /dev/null @@ -1,214 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.monitor - -import android.os.Handler -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.domain.scope.RumApplicationScope -import com.datadog.android.rum.internal.domain.scope.RumRawEvent -import com.datadog.android.rum.internal.domain.scope.RumScope -import java.util.UUID -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - -internal class DatadogRumMonitor( - applicationId: UUID, - internal val samplingRate: Float, - private val writer: Writer, - internal val handler: Handler, - firstPartyHostDetector: FirstPartyHostDetector -) : RumMonitor, AdvancedRumMonitor { - - internal val rootScope: RumScope = RumApplicationScope( - applicationId, - samplingRate, - firstPartyHostDetector - ) - - internal val keepAliveRunnable = Runnable { - handleEvent(RumRawEvent.KeepAlive()) - } - - init { - handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_MS) - } - - private val executorService: ExecutorService = Executors.newSingleThreadExecutor() - - // region RumMonitor - - override fun startView(key: Any, name: String, attributes: Map) { - handleEvent( - RumRawEvent.StartView(key, name, attributes) - ) - } - - override fun stopView(key: Any, attributes: Map) { - handleEvent( - RumRawEvent.StopView(key, attributes) - ) - } - - override fun addUserAction(type: RumActionType, name: String, attributes: Map) { - handleEvent( - RumRawEvent.StartAction(type, name, false, attributes) - ) - } - - override fun startUserAction(type: RumActionType, name: String, attributes: Map) { - handleEvent( - RumRawEvent.StartAction(type, name, true, attributes) - ) - } - - override fun stopUserAction(type: RumActionType, name: String, attributes: Map) { - handleEvent( - RumRawEvent.StopAction(type, name, attributes) - ) - } - - override fun startResource( - key: String, - method: String, - url: String, - attributes: Map - ) { - handleEvent( - RumRawEvent.StartResource(key, url, method, attributes) - ) - } - - override fun stopResource( - key: String, - statusCode: Int?, - size: Long?, - kind: RumResourceKind, - attributes: Map - ) { - handleEvent( - RumRawEvent.StopResource(key, statusCode?.toLong(), size, kind, attributes) - ) - } - - override fun stopResourceWithError( - key: String, - statusCode: Int?, - message: String, - source: RumErrorSource, - throwable: Throwable - ) { - handleEvent( - RumRawEvent.StopResourceWithError(key, statusCode?.toLong(), message, source, throwable) - ) - } - - override fun addError( - message: String, - source: RumErrorSource, - throwable: Throwable?, - attributes: Map - ) { - handleEvent( - RumRawEvent.AddError(message, source, throwable, null, false, attributes) - ) - } - - override fun addErrorWithStacktrace( - message: String, - source: RumErrorSource, - stacktrace: String?, - attributes: Map - ) { - handleEvent( - RumRawEvent.AddError(message, source, null, stacktrace, false, attributes) - ) - } - - // endregion - - // region AdvancedRumMonitor - - override fun resetSession() { - handleEvent( - RumRawEvent.ResetSession() - ) - } - - override fun viewTreeChanged(eventTime: Time) { - handleEvent( - RumRawEvent.ViewTreeChanged(eventTime) - ) - } - - override fun waitForResourceTiming(key: String) { - handleEvent( - RumRawEvent.WaitForResourceTiming(key) - ) - } - - override fun addResourceTiming(key: String, timing: ResourceTiming) { - handleEvent( - RumRawEvent.AddResourceTiming(key, timing) - ) - } - - override fun addCrash(message: String, source: RumErrorSource, throwable: Throwable) { - handleEvent( - RumRawEvent.AddError(message, source, throwable, null, true, emptyMap()) - ) - } - - override fun updateViewLoadingTime( - key: Any, - loadingTimeInNs: Long, - type: ViewEvent.LoadingType - ) { - handleEvent( - RumRawEvent.UpdateViewLoadingTime(key, loadingTimeInNs, type) - ) - } - - override fun addTiming(name: String) { - handleEvent( - RumRawEvent.AddCustomTiming(name) - ) - } - - // endregion - - // region Internal - - internal fun handleEvent(event: RumRawEvent) { - handler.removeCallbacks(keepAliveRunnable) - executorService.submit { - synchronized(rootScope) { - rootScope.handleEvent(event, writer) - } - handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_MS) - } - } - - internal fun stopKeepAliveCallback() { - handler.removeCallbacks(keepAliveRunnable) - } - - // endregion - - companion object { - internal val KEEP_ALIVE_MS = TimeUnit.MINUTES.toMillis(5) - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/net/RumOkHttpUploader.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/net/RumOkHttpUploader.kt deleted file mode 100644 index 9af04f0d1b..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/net/RumOkHttpUploader.kt +++ /dev/null @@ -1,62 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.net - -import com.datadog.android.BuildConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.net.DataOkHttpUploader -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.internal.RumFeature -import java.util.Locale -import okhttp3.OkHttpClient - -internal open class RumOkHttpUploader( - endpoint: String, - private val token: String, - client: OkHttpClient -) : DataOkHttpUploader(buildUrl(endpoint, token), client, CONTENT_TYPE_TEXT_UTF8) { - - private val tags: String by lazy { - arrayOf( - "${RumAttributes.SERVICE_NAME}:${CoreFeature.serviceName}", - "${RumAttributes.APPLICATION_VERSION}:${CoreFeature.packageVersion}", - "${RumAttributes.SDK_VERSION}:${BuildConfig.VERSION_NAME}", - "${RumAttributes.ENV}:${RumFeature.envName}" - ).joinToString(",") - } - - // region DataOkHttpUploader - - override fun setEndpoint(endpoint: String) { - super.setEndpoint(buildUrl(endpoint, token)) - } - - override fun buildQueryParams(): MutableMap { - return mutableMapOf( - QP_BATCH_TIME to System.currentTimeMillis(), - QP_SOURCE to DD_SOURCE_ANDROID, - QP_TAGS to tags - ) - } - - // endregion - - companion object { - internal const val QP_TAGS = "ddtags" - internal const val UPLOAD_URL = - "%s/v1/input/%s" - - private fun buildUrl(endpoint: String, token: String): String { - return String.format( - Locale.US, - UPLOAD_URL, - endpoint, - token - ) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/AndroidXFragmentLifecycleCallbacks.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/AndroidXFragmentLifecycleCallbacks.kt deleted file mode 100644 index e7ab629ec5..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/AndroidXFragmentLifecycleCallbacks.kt +++ /dev/null @@ -1,129 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.tracking - -import android.content.Context -import android.os.Bundle -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.fragment.app.FragmentManager -import com.datadog.android.core.internal.utils.resolveViewName -import com.datadog.android.core.internal.utils.runIfValid -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.tracking.ComponentPredicate - -internal open class AndroidXFragmentLifecycleCallbacks( - internal val argumentsProvider: (Fragment) -> Map, - private val componentPredicate: ComponentPredicate, - private val viewLoadingTimer: ViewLoadingTimer = ViewLoadingTimer(), - private val rumMonitor: RumMonitor, - private val advancedRumMonitor: AdvancedRumMonitor -) : FragmentLifecycleCallbacks, FragmentManager.FragmentLifecycleCallbacks() { - - // region FragmentLifecycleCallbacks - - override fun register(activity: FragmentActivity) { - activity.supportFragmentManager.registerFragmentLifecycleCallbacks(this, true) - } - - override fun unregister(activity: FragmentActivity) { - activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(this) - } - - // endregion - - // region FragmentManager.FragmentLifecycleCallbacks - - override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) { - super.onFragmentAttached(fm, f, context) - componentPredicate.runIfValid(f) { - viewLoadingTimer.onCreated(resolveKey(it)) - } - } - - override fun onFragmentStarted(fm: FragmentManager, f: Fragment) { - super.onFragmentStarted(fm, f) - componentPredicate.runIfValid(f) { - viewLoadingTimer.onStartLoading(resolveKey(it)) - } - } - - override fun onFragmentActivityCreated( - fm: FragmentManager, - f: Fragment, - savedInstanceState: Bundle? - ) { - super.onFragmentActivityCreated(fm, f, savedInstanceState) - - val context = f.context - - if (f is DialogFragment && context != null) { - val window = f.dialog?.window - val gesturesTracker = RumFeature.gesturesTracker - gesturesTracker.startTracking(window, context) - } - } - - override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { - super.onFragmentResumed(fm, f) - componentPredicate.runIfValid(f) { - val key = resolveKey(it) - viewLoadingTimer.onFinishedLoading(key) - rumMonitor.startView( - key, - it.resolveViewName(), - argumentsProvider(it) - ) - val loadingTime = viewLoadingTimer.getLoadingTime(key) - if (loadingTime != null) { - advancedRumMonitor.updateViewLoadingTime( - key, - loadingTime, - resolveLoadingType(viewLoadingTimer.isFirstTimeLoading(key)) - ) - } - } - } - - override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { - super.onFragmentPaused(fm, f) - componentPredicate.runIfValid(f) { - val key = resolveKey(it) - rumMonitor.stopView(key) - viewLoadingTimer.onPaused(key) - } - } - - override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) { - super.onFragmentDestroyed(fm, f) - componentPredicate.runIfValid(f) { - viewLoadingTimer.onDestroyed(resolveKey(it)) - } - } - - // endregion - - // region utils - - open fun resolveKey(fragment: Fragment): Any { - return fragment - } - - private fun resolveLoadingType(firstTimeLoading: Boolean): ViewEvent.LoadingType { - return if (firstTimeLoading) { - ViewEvent.LoadingType.FRAGMENT_DISPLAY - } else { - ViewEvent.LoadingType.FRAGMENT_REDISPLAY - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacks.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacks.kt deleted file mode 100644 index b056266df0..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacks.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.datadog.android.rum.internal.tracking - -import android.app.Activity -import android.app.DialogFragment -import android.app.Fragment -import android.app.FragmentManager -import android.content.Context -import android.os.Build -import android.os.Bundle -import androidx.annotation.RequiresApi -import com.datadog.android.core.internal.utils.resolveViewName -import com.datadog.android.core.internal.utils.runIfValid -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.tracking.ComponentPredicate - -@Suppress("DEPRECATION") -@RequiresApi(Build.VERSION_CODES.O) -internal class OreoFragmentLifecycleCallbacks( - private val argumentsProvider: (Fragment) -> Map, - private val componentPredicate: ComponentPredicate, - private val viewLoadingTimer: ViewLoadingTimer = ViewLoadingTimer(), - private val rumMonitor: RumMonitor, - private val advancedRumMonitor: AdvancedRumMonitor -) : FragmentLifecycleCallbacks, FragmentManager.FragmentLifecycleCallbacks() { - - // region FragmentLifecycleCallbacks - - override fun register(activity: Activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity.fragmentManager.registerFragmentLifecycleCallbacks(this, true) - } - } - - override fun unregister(activity: Activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity.fragmentManager.unregisterFragmentLifecycleCallbacks(this) - } - } - - // endregion - - // region FragmentManager.FragmentLifecycleCallbacks - - override fun onFragmentActivityCreated( - fm: FragmentManager, - f: Fragment, - savedInstanceState: Bundle? - ) { - super.onFragmentActivityCreated(fm, f, savedInstanceState) - - val context = f.context - - if (f is DialogFragment && context != null) { - val window = f.dialog?.window - val gesturesTracker = RumFeature.gesturesTracker - gesturesTracker.startTracking(window, context) - } - } - - override fun onFragmentAttached(fm: FragmentManager?, f: Fragment, context: Context?) { - super.onFragmentAttached(fm, f, context) - componentPredicate.runIfValid(f) { - viewLoadingTimer.onCreated(it) - } - } - - override fun onFragmentStarted(fm: FragmentManager?, f: Fragment) { - super.onFragmentStarted(fm, f) - componentPredicate.runIfValid(f) { - viewLoadingTimer.onStartLoading(it) - } - } - - override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { - super.onFragmentResumed(fm, f) - componentPredicate.runIfValid(f) { - viewLoadingTimer.onFinishedLoading(f) - rumMonitor.startView(it, it.resolveViewName(), argumentsProvider(it)) - val loadingTime = viewLoadingTimer.getLoadingTime(it) - if (loadingTime != null) { - advancedRumMonitor.updateViewLoadingTime( - it, - loadingTime, - resolveLoadingType(viewLoadingTimer.isFirstTimeLoading(it)) - ) - } - } - } - - override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { - super.onFragmentPaused(fm, f) - componentPredicate.runIfValid(f) { - rumMonitor.stopView(it) - viewLoadingTimer.onPaused(it) - } - } - - override fun onFragmentDestroyed(fm: FragmentManager?, f: Fragment) { - super.onFragmentDestroyed(fm, f) - componentPredicate.runIfValid(f) { - viewLoadingTimer.onDestroyed(it) - } - } - - // endregion - - // region Internal - - private fun resolveLoadingType(firstTimeLoading: Boolean): ViewEvent.LoadingType { - return if (firstTimeLoading) { - ViewEvent.LoadingType.FRAGMENT_DISPLAY - } else { - ViewEvent.LoadingType.FRAGMENT_REDISPLAY - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/UserActionTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/UserActionTrackingStrategy.kt deleted file mode 100644 index 8648239b10..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/UserActionTrackingStrategy.kt +++ /dev/null @@ -1,16 +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 2016-Present Datadog, Inc. - */ -package com.datadog.android.rum.internal.tracking - -import com.datadog.android.rum.tracking.TrackingStrategy -import com.datadog.tools.annotation.NoOpImplementation - -/** - * A TrackingStrategy dedicated to user actions tracking. - */ -@NoOpImplementation -internal interface UserActionTrackingStrategy : - TrackingStrategy diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/ViewLoadingTimer.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/ViewLoadingTimer.kt deleted file mode 100644 index c977fcc9d0..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/ViewLoadingTimer.kt +++ /dev/null @@ -1,74 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.tracking - -import java.util.WeakHashMap - -internal class ViewLoadingTimer { - private val viewsTimeAndState = WeakHashMap() - - fun onCreated(view: Any) { - viewsTimeAndState[view] = ViewLoadingInfo(System.nanoTime()) - } - - fun onStartLoading(view: Any) { - viewsTimeAndState[view]?.let { - if (it.loadingStart == null) { - it.loadingStart = System.nanoTime() - } - } - } - - fun onFinishedLoading(view: Any) { - viewsTimeAndState[view]?.let { - val loadingStart = it.loadingStart - it.loadingTime = - if (loadingStart != null) { - System.nanoTime() - loadingStart - } else { - // in case the view was hidden but it was resumed directly - // without being started again - 0 - } - if (it.finishedLoadingOnce) { - it.firstTimeLoading = false - } - } - } - - fun onDestroyed(view: Any) { - viewsTimeAndState.remove(view) - } - - fun onPaused(view: Any) { - viewsTimeAndState[view]?.let { - it.loadingTime = 0 - it.loadingStart = null - it.firstTimeLoading = false - it.finishedLoadingOnce = true - } - } - - fun getLoadingTime(view: Any): Long? { - viewsTimeAndState[view]?.let { - return it.loadingTime - } - - return null - } - - fun isFirstTimeLoading(view: Any): Boolean { - return viewsTimeAndState[view]?.firstTimeLoading ?: false - } - - private data class ViewLoadingInfo( - var loadingStart: Long?, - var loadingTime: Long = 0, - var firstTimeLoading: Boolean = true, - var finishedLoadingOnce: Boolean = false - ) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/ViewTreeChangeTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/ViewTreeChangeTrackingStrategy.kt deleted file mode 100644 index 412575d8f8..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/internal/tracking/ViewTreeChangeTrackingStrategy.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.datadog.android.rum.internal.tracking - -import android.app.Activity -import android.view.ViewTreeObserver -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.tracking.ActivityLifecycleTrackingStrategy -import com.datadog.android.rum.tracking.TrackingStrategy - -internal class ViewTreeChangeTrackingStrategy : - ActivityLifecycleTrackingStrategy(), - TrackingStrategy, - ViewTreeObserver.OnGlobalLayoutListener { - - // region ActivityLifecycleTrackingStrategy - - override fun onActivityStarted(activity: Activity) { - super.onActivityStarted(activity) - - val viewTreeObserver = getViewTreeObserver(activity) - viewTreeObserver?.addOnGlobalLayoutListener(this) - } - - override fun onActivityStopped(activity: Activity) { - super.onActivityStopped(activity) - - val viewTreeObserver = getViewTreeObserver(activity) - viewTreeObserver?.removeOnGlobalLayoutListener(this) - } - - // endregion - - // region ViewTreeObserver.OnGlobalLayoutListener - - override fun onGlobalLayout() { - val now = Time() - (GlobalRum.get() as? AdvancedRumMonitor)?.viewTreeChanged(now) - } - - // endregion - - // region Internal - - private fun getViewTreeObserver(activity: Activity): ViewTreeObserver? { - val window = activity.window ?: return null - return window.decorView.viewTreeObserver - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllActivities.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllActivities.kt deleted file mode 100644 index cbdb098046..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllActivities.kt +++ /dev/null @@ -1,21 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import android.app.Activity - -/** - * A predefined [ComponentPredicate] which accepts all [Activity] - * to be tracked as a RUM View event. - * This is the default behaviour for the [ActivityViewTrackingStrategy]. - */ -class AcceptAllActivities : ComponentPredicate { - - override fun accept(component: Activity): Boolean { - return true - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllDefaultFragment.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllDefaultFragment.kt deleted file mode 100644 index da9393a3d4..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllDefaultFragment.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import android.app.Fragment - -/** - * A predefined [ComponentPredicate] which accepts all [Fragment] to be tracked as RUM View event. - * This is the default behaviour for the [FragmentViewTrackingStrategy]. - */ -class AcceptAllDefaultFragment : ComponentPredicate { - - override fun accept(component: Fragment): Boolean { - return true - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllSupportFragments.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllSupportFragments.kt deleted file mode 100644 index 9e4fb0e6f2..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/AcceptAllSupportFragments.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import androidx.fragment.app.Fragment - -/** - * A predefined [ComponentPredicate] which accepts all [Fragment] to be tracked as RUM View - * event. This is the default behaviour of the [FragmentViewTrackingStrategy]. - */ -class AcceptAllSupportFragments : ComponentPredicate { - - override fun accept(component: Fragment): Boolean { - return true - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy.kt deleted file mode 100644 index 485778f51c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy.kt +++ /dev/null @@ -1,99 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import android.app.Activity -import android.app.Application -import android.content.Context -import android.os.Bundle -import com.datadog.android.core.internal.utils.devLogger - -/** - * The ActivityLifecycleTrackingStrategy as an [Application.ActivityLifecycleCallbacks] - * based implementation of the [TrackingStrategy]. - */ -abstract class ActivityLifecycleTrackingStrategy : - Application.ActivityLifecycleCallbacks, - TrackingStrategy { - - // region TrackingStrategy - - override fun register(context: Context) { - if (context is Application) { - context.registerActivityLifecycleCallbacks(this) - } else { - devLogger.e( - "In order to use the RUM automatic tracking feature you will have" + - "to use the Application context when initializing the SDK" - ) - } - } - - override fun unregister(context: Context?) { - if (context is Application) { - context.unregisterActivityLifecycleCallbacks(this) - } - } - - // endregion - - // region Application.ActivityLifecycleCallbacks - - override fun onActivityPaused(activity: Activity) { - // No Op - } - - override fun onActivityStarted(activity: Activity) { - // No Op - } - - override fun onActivityDestroyed(activity: Activity) { - // No Op - } - - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { - // No Op - } - - override fun onActivityStopped(activity: Activity) { - // No Op - } - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - // No Op - } - - override fun onActivityResumed(activity: Activity) { - // No Op - } - - // endregion - - // region Utils - - /** - * Maps the Bundle key - value properties into compatible attributes for the Rum Events. - * @param bundle the Bundle we need to transform. Returns an empty Map if this is null. - */ - protected fun convertToRumAttributes(bundle: Bundle?): Map { - if (bundle == null) return emptyMap() - - val attributes = mutableMapOf() - - bundle.keySet().forEach { - attributes["$ARGUMENT_TAG.$it"] = bundle.get(it) - } - - return attributes - } - - // endregion - - companion object { - internal const val ARGUMENT_TAG = "view.arguments" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ActivityViewTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ActivityViewTrackingStrategy.kt deleted file mode 100644 index 4e68f160cd..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ActivityViewTrackingStrategy.kt +++ /dev/null @@ -1,119 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import android.app.Activity -import android.os.Bundle -import com.datadog.android.core.internal.utils.runIfValid -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.tracking.ViewLoadingTimer - -/** - * A [ViewTrackingStrategy] that will track [Activity] as RUM Views. - * - * Each activity's lifecycle will be monitored to start and stop RUM Views when relevant. - * @param trackExtras whether to track Activity Intent extras - * @param componentPredicate to accept the Activities that will be taken into account as - * valid RUM View events. - */ -class ActivityViewTrackingStrategy @JvmOverloads constructor( - private val trackExtras: Boolean, - private val componentPredicate: ComponentPredicate = AcceptAllActivities() -) : - ActivityLifecycleTrackingStrategy(), - ViewTrackingStrategy { - - private val viewLoadingTimer = ViewLoadingTimer() - - // region ActivityLifecycleTrackingStrategy - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - super.onActivityCreated(activity, savedInstanceState) - componentPredicate.runIfValid(activity) { - viewLoadingTimer.onCreated(it) - } - } - - override fun onActivityStarted(activity: Activity) { - super.onActivityStarted(activity) - componentPredicate.runIfValid(activity) { - viewLoadingTimer.onStartLoading(it) - } - } - - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - componentPredicate.runIfValid(activity) { - val javaClass = it.javaClass - val vieName = javaClass.canonicalName ?: javaClass.simpleName - val attributes = - if (trackExtras) convertToRumAttributes(it.intent?.extras) else emptyMap() - GlobalRum.monitor.startView( - it, - vieName, - attributes - ) - // we still need to call onFinishedLoading here for API bellow 29 as the - // onPostResumed is not available on these devices. - viewLoadingTimer.onFinishedLoading(it) - } - } - - override fun onActivityPostResumed(activity: Activity) { - super.onActivityPostResumed(activity) - // this method is only available from API 29 and above - componentPredicate.runIfValid(activity) { - viewLoadingTimer.onFinishedLoading(it) - } - } - - override fun onActivityPaused(activity: Activity) { - super.onActivityPaused(activity) - componentPredicate.runIfValid(activity) { - updateLoadingTime(activity) - GlobalRum.monitor.stopView(it) - viewLoadingTimer.onPaused(activity) - } - } - - override fun onActivityDestroyed(activity: Activity) { - super.onActivityDestroyed(activity) - componentPredicate.runIfValid(activity) { - viewLoadingTimer.onDestroyed(it) - } - } - - // endregion - - // region Internal - - private fun updateLoadingTime(activity: Activity) { - viewLoadingTimer.getLoadingTime(activity)?.let { loadingTime -> - val advancedRumMonitor = GlobalRum.get() as? AdvancedRumMonitor - advancedRumMonitor?.let { monitor -> - val loadingType = resolveLoadingType(viewLoadingTimer.isFirstTimeLoading(activity)) - monitor.updateViewLoadingTime( - activity, - loadingTime, - loadingType - ) - } - } - } - - private fun resolveLoadingType(firstTimeLoading: Boolean): ViewEvent.LoadingType { - return if (firstTimeLoading) { - ViewEvent.LoadingType.ACTIVITY_DISPLAY - } else { - ViewEvent.LoadingType.ACTIVITY_REDISPLAY - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt deleted file mode 100644 index afd672fa72..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt +++ /dev/null @@ -1,96 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import android.app.Activity -import android.os.Build -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.monitor.NoOpAdvancedRumMonitor -import com.datadog.android.rum.internal.tracking.AndroidXFragmentLifecycleCallbacks -import com.datadog.android.rum.internal.tracking.FragmentLifecycleCallbacks -import com.datadog.android.rum.internal.tracking.NoOpFragmentLifecycleCallbacks -import com.datadog.android.rum.internal.tracking.OreoFragmentLifecycleCallbacks - -/** - * A [ViewTrackingStrategy] that will track [Fragment]s as RUM Views. - * - * Each fragment's lifecycle will be monitored to start and stop RUM Views when relevant. - * - * **Note**: This version of the [FragmentViewTrackingStrategy] is compatible with - * the AndroidX Compat Library. - * @param trackArguments whether we track Fragment arguments - * @param supportFragmentComponentPredicate to accept the Androidx Fragments - * that will be taken into account as valid RUM View events. - * @param defaultFragmentComponentPredicate to accept the default Android Fragments - * that will be taken into account as valid RUM View events. - */ -class FragmentViewTrackingStrategy @JvmOverloads constructor( - private val trackArguments: Boolean, - private val supportFragmentComponentPredicate: ComponentPredicate = - AcceptAllSupportFragments(), - private val defaultFragmentComponentPredicate: ComponentPredicate = - AcceptAllDefaultFragment() -) : - ActivityLifecycleTrackingStrategy(), - ViewTrackingStrategy { - - private val androidXLifecycleCallbacks: FragmentLifecycleCallbacks - by lazy { - AndroidXFragmentLifecycleCallbacks( - argumentsProvider = { - if (trackArguments) convertToRumAttributes(it.arguments) else emptyMap() - }, - componentPredicate = supportFragmentComponentPredicate, - rumMonitor = GlobalRum.get(), - advancedRumMonitor = GlobalRum.get() as? AdvancedRumMonitor - ?: NoOpAdvancedRumMonitor() - ) - } - private val oreoLifecycleCallbacks: FragmentLifecycleCallbacks - by lazy { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - OreoFragmentLifecycleCallbacks( - argumentsProvider = { - if (trackArguments) convertToRumAttributes(it.arguments) else emptyMap() - }, - componentPredicate = defaultFragmentComponentPredicate, - rumMonitor = GlobalRum.get(), - advancedRumMonitor = GlobalRum.get() as? AdvancedRumMonitor - ?: NoOpAdvancedRumMonitor() - ) - } else { - NoOpFragmentLifecycleCallbacks() - } - } - - // region ActivityLifecycleTrackingStrategy - - override fun onActivityStarted(activity: Activity) { - super.onActivityStarted(activity) - if (FragmentActivity::class.java.isAssignableFrom(activity::class.java)) { - androidXLifecycleCallbacks.register(activity as FragmentActivity) - } else { - // old deprecated way - oreoLifecycleCallbacks.register(activity) - } - } - - override fun onActivityStopped(activity: Activity) { - super.onActivityStopped(activity) - if (FragmentActivity::class.java.isAssignableFrom(activity::class.java)) { - androidXLifecycleCallbacks.unregister(activity as FragmentActivity) - } else { - // old deprecated way - oreoLifecycleCallbacks.unregister(activity) - } - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/MixedViewTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/MixedViewTrackingStrategy.kt deleted file mode 100644 index 97c86f8e2a..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/MixedViewTrackingStrategy.kt +++ /dev/null @@ -1,93 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import android.app.Activity -import android.os.Bundle -import androidx.fragment.app.Fragment - -/** - * A [ViewTrackingStrategy] that will track [Activity] and [Fragment] as RUM View Events. - * This strategy will apply both the [ActivityViewTrackingStrategy] - * and the [FragmentViewTrackingStrategy] and will remain for you to decide whether to exclude - * some activities or fragments from tracking by providing an implementation for the right - * predicate in the constructor arguments. - * @see ActivityViewTrackingStrategy - * @see FragmentViewTrackingStrategy - * **Note**: This version of the [MixedViewTrackingStrategy] is compatible with - * the AndroidX Compat Library. - * @param trackExtras whether to track Activity Intent extras or the Fragment arguments. - * @param componentPredicate to accept the Activities that will be taken into account as - * valid RUM View events. - * @param supportFragmentComponentPredicate to accept the Androidx Fragments - * that will be taken into account as valid RUM View events. - * @param defaultFragmentComponentPredicate to accept the default Android Fragments - * that will be taken into account as valid RUM View events. - */ -class MixedViewTrackingStrategy internal constructor( - private val activityViewTrackingStrategy: ActivityViewTrackingStrategy, - private val fragmentViewTrackingStrategy: FragmentViewTrackingStrategy -) : ActivityLifecycleTrackingStrategy(), - ViewTrackingStrategy { - - @JvmOverloads - constructor( - trackExtras: Boolean, - componentPredicate: ComponentPredicate = AcceptAllActivities(), - supportFragmentComponentPredicate: ComponentPredicate = - AcceptAllSupportFragments(), - defaultFragmentComponentPredicate: ComponentPredicate = - AcceptAllDefaultFragment() - ) : this( - ActivityViewTrackingStrategy(trackExtras, componentPredicate), - FragmentViewTrackingStrategy( - trackExtras, - supportFragmentComponentPredicate, - defaultFragmentComponentPredicate - ) - ) - - // region ActivityLifecycleTrackingStrategy - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - super.onActivityCreated(activity, savedInstanceState) - activityViewTrackingStrategy.onActivityCreated(activity, savedInstanceState) - fragmentViewTrackingStrategy.onActivityCreated(activity, savedInstanceState) - } - - override fun onActivityStarted(activity: Activity) { - super.onActivityStarted(activity) - activityViewTrackingStrategy.onActivityStarted(activity) - fragmentViewTrackingStrategy.onActivityStarted(activity) - } - - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - activityViewTrackingStrategy.onActivityResumed(activity) - fragmentViewTrackingStrategy.onActivityResumed(activity) - } - - override fun onActivityPaused(activity: Activity) { - super.onActivityPaused(activity) - activityViewTrackingStrategy.onActivityPaused(activity) - fragmentViewTrackingStrategy.onActivityPaused(activity) - } - - override fun onActivityStopped(activity: Activity) { - super.onActivityStopped(activity) - activityViewTrackingStrategy.onActivityStopped(activity) - fragmentViewTrackingStrategy.onActivityStopped(activity) - } - - override fun onActivityDestroyed(activity: Activity) { - super.onActivityDestroyed(activity) - activityViewTrackingStrategy.onActivityDestroyed(activity) - fragmentViewTrackingStrategy.onActivityDestroyed(activity) - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/NavigationViewTrackingStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/NavigationViewTrackingStrategy.kt deleted file mode 100644 index 768fee08e5..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/NavigationViewTrackingStrategy.kt +++ /dev/null @@ -1,173 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.tracking - -import android.app.Activity -import android.os.Bundle -import androidx.annotation.IdRes -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.navigation.ActivityNavigator -import androidx.navigation.NavController -import androidx.navigation.NavDestination -import androidx.navigation.findNavController -import androidx.navigation.fragment.DialogFragmentNavigator -import androidx.navigation.fragment.FragmentNavigator -import androidx.navigation.fragment.NavHostFragment -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.monitor.NoOpAdvancedRumMonitor -import com.datadog.android.rum.internal.tracking.AndroidXFragmentLifecycleCallbacks -import java.lang.IllegalStateException -import java.util.WeakHashMap - -/** - * A [ViewTrackingStrategy] that will track [Fragment]s within a NavigationHost - * as RUM Views. - * - * @param navigationViewId the id of the NavHost view within the hosting [Activity]. - * @param trackArguments whether to track navigation arguments - */ -class NavigationViewTrackingStrategy( - @IdRes private val navigationViewId: Int, - private val trackArguments: Boolean -) : - ActivityLifecycleTrackingStrategy(), - ViewTrackingStrategy, - NavController.OnDestinationChangedListener { - - private var lifecycleCallbackRefs = - WeakHashMap() - private val predicate: ComponentPredicate = object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return !NavHostFragment::class.java.isAssignableFrom(component.javaClass) - } - } - - // region ActivityLifecycleTrackingStrategy - - override fun onActivityStarted(activity: Activity) { - super.onActivityStarted(activity) - activity.findNavControllerOrNull(navigationViewId)?.let { - if (FragmentActivity::class.java.isAssignableFrom(activity::class.java)) { - val navControllerFragmentCallbacks = NavControllerFragmentLifecycleCallbacks( - it, - argumentsProvider = { - emptyMap() - }, - componentPredicate = predicate - ) - navControllerFragmentCallbacks.register(activity as FragmentActivity) - lifecycleCallbackRefs[activity] = navControllerFragmentCallbacks - } - it.addOnDestinationChangedListener(this) - } - } - - override fun onActivityStopped(activity: Activity) { - super.onActivityStopped(activity) - activity.findNavControllerOrNull(navigationViewId)?.let { - it.removeOnDestinationChangedListener(this) - if (FragmentActivity::class.java.isAssignableFrom(activity::class.java)) { - lifecycleCallbackRefs.remove(activity)?.unregister(activity as FragmentActivity) - } - } - } - - override fun onActivityPaused(activity: Activity) { - super.onActivityPaused(activity) - activity.findNavControllerOrNull(navigationViewId)?.currentDestination?.let { - GlobalRum.get().stopView(it) - } - } - - // endregion - - // region OnDestinationChangedListener - - override fun onDestinationChanged( - controller: NavController, - destination: NavDestination, - arguments: Bundle? - ) { - val attributes = if (trackArguments) convertToRumAttributes(arguments) else emptyMap() - val name = destination.getRumViewName() - GlobalRum.get().startView(destination, name, attributes) - } - - // endregion - - // region Internal - - @Suppress("SwallowedException") - private fun Activity.findNavControllerOrNull(@IdRes viewId: Int): NavController? { - return try { - findNavController(viewId) - } catch (e: IllegalArgumentException) { - null - } catch (e: IllegalStateException) { - null - } - } - - private fun NavDestination.getRumViewName(): String { - return when (this) { - is FragmentNavigator.Destination -> className - is DialogFragmentNavigator.Destination -> className - is ActivityNavigator.Destination -> { - component?.className ?: UNKNOWN_DESTINATION_NAME - } - else -> UNKNOWN_DESTINATION_NAME - } - } - - // endregion - - // region Internal - - // endregion - - internal class NavControllerFragmentLifecycleCallbacks( - private val navController: NavController, - argumentsProvider: (Fragment) -> Map, - componentPredicate: ComponentPredicate - ) : AndroidXFragmentLifecycleCallbacks( - argumentsProvider, - componentPredicate, - rumMonitor = NoOpRumMonitor(), - advancedRumMonitor = AdvancedMonitorDecorator( - GlobalRum.get() as? AdvancedRumMonitor ?: NoOpAdvancedRumMonitor() - ) - ) { - override fun resolveKey(fragment: Fragment): Any { - return navController.currentDestination ?: NO_DESTINATION_FOUND - } - - companion object { - val NO_DESTINATION_FOUND = Any() - } - } - - internal class AdvancedMonitorDecorator(private val advancedRumMonitor: AdvancedRumMonitor) : - AdvancedRumMonitor by advancedRumMonitor { - override fun updateViewLoadingTime( - key: Any, - loadingTimeInNs: Long, - type: ViewEvent.LoadingType - ) { - if (key != NavControllerFragmentLifecycleCallbacks.NO_DESTINATION_FOUND) { - advancedRumMonitor.updateViewLoadingTime(key, loadingTimeInNs, type) - } - } - } - - companion object { - internal const val UNKNOWN_DESTINATION_NAME = "Unknown" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ViewAttributesProvider.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ViewAttributesProvider.kt deleted file mode 100644 index 2b91b3454b..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/tracking/ViewAttributesProvider.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.datadog.android.rum.tracking - -import android.view.View -import com.datadog.android.rum.RumAttributes - -/** - * Provides the extra attributes for the as Map. - */ -interface ViewAttributesProvider { - - /** - * Add extra attributes to the default attributes Map. - * @param view the [View]. - * @param attributes the default attributes Map. Usually this contains some default - * attributes which are determined and added by the SDK. Please make sure you do not - * override any of these reserved attributes. - * @see [RumAttributes.TAG_TARGET_RESOURCE_ID] - * @see [RumAttributes.TAG_TARGET_CLASS_NAME] - */ - fun extractAttributes(view: View, attributes: MutableMap) -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/webview/RumWebChromeClient.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/webview/RumWebChromeClient.kt deleted file mode 100644 index a50917513f..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/webview/RumWebChromeClient.kt +++ /dev/null @@ -1,80 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.webview - -import android.util.Log -import android.webkit.ConsoleMessage -import android.webkit.WebChromeClient -import android.webkit.WebViewClient -import com.datadog.android.log.Logger -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource - -/** - * A [WebViewClient] propagating all relevant events to Datadog. - * - * Any console message will be forwarded to an internal [Logger], and errors - * will be sent to the [GlobalRum] monitor as RUM Errors. - */ -open class RumWebChromeClient -internal constructor(private val logger: Logger) : WebChromeClient() { - - constructor() : this( - Logger.Builder() - .setLoggerName(LOGGER_NAME) - .setNetworkInfoEnabled(true) - .build() - ) - - // region WebChromeClient - - override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { - if (consoleMessage != null) { - val message = consoleMessage.message() - val level = consoleMessage.messageLevel() - val attributes = mapOf( - SOURCE_ID to consoleMessage.sourceId(), - SOURCE_LINE to consoleMessage.lineNumber() - ) - - if (level == ConsoleMessage.MessageLevel.ERROR) { - GlobalRum.get().addError( - message, - RumErrorSource.WEBVIEW, - null, - attributes - ) - } else { - logger.log(level.toLogLevel(), message, null, attributes) - } - } - return false - } - - // endregion - - // region Internal - - private fun ConsoleMessage.MessageLevel.toLogLevel(): Int { - return when (this) { - ConsoleMessage.MessageLevel.LOG -> Log.VERBOSE - ConsoleMessage.MessageLevel.DEBUG -> Log.DEBUG - ConsoleMessage.MessageLevel.TIP -> Log.INFO - ConsoleMessage.MessageLevel.WARNING -> Log.WARN - ConsoleMessage.MessageLevel.ERROR -> Log.ERROR - } - } - - // endregion - - companion object { - internal const val LOGGER_NAME = "WebChromeClient" - - internal const val SOURCE_ID = "source.id" - internal const val SOURCE_LINE = "source.line" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/webview/RumWebViewClient.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/webview/RumWebViewClient.kt deleted file mode 100644 index c25d4eec3e..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/rum/webview/RumWebViewClient.kt +++ /dev/null @@ -1,130 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.webview - -import android.graphics.Bitmap -import android.net.http.SslError -import android.os.Build -import android.webkit.SslErrorHandler -import android.webkit.WebResourceError -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.annotation.RequiresApi -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind - -/** - * A [WebViewClient] propagating all relevant events to the [GlobalRum] monitor. - * - * This will map the page loading, and webview errors into RUM Resource and - * Error events respectively. - */ -open class RumWebViewClient : WebViewClient() { - - // region WebViewClient - - /** @inheritdoc */ - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - if (url != null) { - val key = url - GlobalRum.get().startResource( - key, - METHOD_GET, - url - ) - } - } - - /** @inheritdoc */ - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - if (url != null) { - GlobalRum.get().stopResource( - url, - 200, - null, - RumResourceKind.DOCUMENT, - emptyMap() - ) - } - } - - /** @inheritdoc */ - override fun onReceivedError( - view: WebView?, - errorCode: Int, - description: String?, - failingUrl: String? - ) { - super.onReceivedError(view, errorCode, description, failingUrl) - GlobalRum.get().addError( - "Error $errorCode: $description", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to failingUrl) - ) - } - - /** @inheritdoc */ - @RequiresApi(Build.VERSION_CODES.M) - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError? - ) { - super.onReceivedError(view, request, error) - GlobalRum.get().addError( - "Error ${error?.errorCode}: ${error?.description}", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to request?.url) - ) - } - - /** @inheritdoc */ - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - override fun onReceivedHttpError( - view: WebView?, - request: WebResourceRequest?, - errorResponse: WebResourceResponse? - ) { - super.onReceivedHttpError(view, request, errorResponse) - GlobalRum.get().addError( - "Error ${errorResponse?.statusCode}: ${errorResponse?.reasonPhrase}", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to request?.url) - ) - } - - /** @inheritdoc */ - override fun onReceivedSslError( - view: WebView?, - handler: SslErrorHandler?, - error: SslError? - ) { - super.onReceivedSslError(view, handler, error) - GlobalRum.get().addError( - "SSL Error ${error?.primaryError}", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to error?.url) - ) - } - - // endregion - - companion object { - internal const val ORIGIN = "WebViewClient" - internal const val METHOD_GET = "GET" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandler.kt deleted file mode 100644 index e0e7ea6e1d..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandler.kt +++ /dev/null @@ -1,47 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.sqlite - -import android.database.DatabaseErrorHandler -import android.database.DefaultDatabaseErrorHandler -import android.database.sqlite.SQLiteDatabase -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import java.util.Locale - -/** - * Provides an implementation of [DatadogDatabaseErrorHandler] already set up to send - * relevant information to Datadog. - * - * It will automatically send RUM Error events whenever a Database corruption was signaled. - * For more information [https://www.sqlite.org/howtocorrupt.html] - */ -class DatadogDatabaseErrorHandler( - internal val defaultErrorHandler: DatabaseErrorHandler = DefaultDatabaseErrorHandler() -) : DatabaseErrorHandler { - - /** @inheritDoc */ - override fun onCorruption(dbObj: SQLiteDatabase) { - defaultErrorHandler.onCorruption(dbObj) - GlobalRum.get() - .addError( - String.format(DATABASE_CORRUPTION_ERROR_MESSAGE, dbObj.path, Locale.US), - RumErrorSource.SOURCE, - null, - mapOf( - RumAttributes.ERROR_DATABASE_PATH to dbObj.path, - RumAttributes.ERROR_DATABASE_VERSION to dbObj.version - ) - ) - } - - companion object { - internal const val DATABASE_CORRUPTION_ERROR_MESSAGE = - "Corruption reported by sqlite database: %s" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/AndroidTracer.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/AndroidTracer.kt deleted file mode 100644 index 7540840344..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/AndroidTracer.kt +++ /dev/null @@ -1,205 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing - -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.Logger -import com.datadog.android.rum.GlobalRum -import com.datadog.android.tracing.internal.TracesFeature -import com.datadog.android.tracing.internal.data.TraceWriter -import com.datadog.android.tracing.internal.handlers.AndroidSpanLogsHandler -import com.datadog.opentracing.DDTracer -import com.datadog.opentracing.LogHandler -import com.datadog.trace.api.Config -import io.opentracing.Span -import io.opentracing.log.Fields -import java.security.SecureRandom -import java.util.Properties -import java.util.Random - -/** - * A class enabling Datadog tracing features. - * - * It allows you to create [ DDSpan ] and send them to Datadog servers. - * - * You can have multiple tracers configured in your application, each with their own settings. - * - */ -class AndroidTracer internal constructor( - config: Config, - writer: TraceWriter, - random: Random, - private val logsHandler: LogHandler, - private val bundleWithRum: Boolean -) : DDTracer(config, writer, random) { - - // region Tracer - - override fun buildSpan(operationName: String): DDSpanBuilder { - return DDSpanBuilder(operationName, scopeManager()) - .withLogHandler(logsHandler) - .withRumContext() - } - - // endregion - - /** - * Builds a [AndroidTracer] instance. - * - */ - class Builder { - - private var bundleWithRumEnabled: Boolean = true - private var serviceName: String = CoreFeature.serviceName - private var partialFlushThreshold = DEFAULT_PARTIAL_MIN_FLUSH - private var random: Random = SecureRandom() - private val logsHandler: LogHandler - - private val globalTags: MutableMap = mutableMapOf() - - init { - val logger = Logger.Builder().setLoggerName(TRACE_LOGGER_NAME).build() - logsHandler = AndroidSpanLogsHandler(logger) - } - - // region Public API - - /** - * Builds a [AndroidTracer] based on the current state of this Builder. - */ - fun build(): AndroidTracer { - return AndroidTracer( - config(), - TraceWriter(TracesFeature.persistenceStrategy.getWriter()), - random, - logsHandler, - bundleWithRumEnabled - ) - } - - /** - * Sets the service name that will appear in your traces. - * @param serviceName the service name (default = "android") - */ - fun setServiceName(serviceName: String): Builder { - this.serviceName = serviceName - return this - } - - /** - * Sets the partial flush threshold. When this threshold is reached (you have a specific - * amount of spans closed waiting) the flush mechanism will be triggered and all the pending - * closed spans will be processed in order to be sent to the intake. - * @param threshold the threshold value (default = 5) - */ - fun setPartialFlushThreshold(threshold: Int): Builder { - this.partialFlushThreshold = threshold - return this - } - - /** - * Adds a global tag which will be appended to all spans created with the built tracer. - * @param key the tag key - * @param value the tag value - */ - fun addGlobalTag(key: String, value: String): Builder { - this.globalTags[key] = value - return this - } - - /** - * Enables the trace bundling with the current active View. If this feature is enabled all - * the spans from this moment on will be bundled with the current view information and you - * will be able to see all the traces sent during a specific view in the Rum Explorer. - * @param enabled true by default - */ - fun setBundleWithRumEnabled(enabled: Boolean): Builder { - bundleWithRumEnabled = enabled - return this - } - - // endregion - - // region Internal - - internal fun withRandom(random: Random): Builder { - this.random = random - return this - } - - internal fun properties(): Properties { - val properties = Properties() - properties.setProperty(Config.SERVICE_NAME, serviceName) - properties.setProperty( - Config.PARTIAL_FLUSH_MIN_SPANS, - partialFlushThreshold.toString() - ) - properties.setProperty( - Config.TAGS, - globalTags.map { "${it.key}:${it.value}" }.joinToString(",") - ) - return properties - } - - private fun config(): Config { - return Config.get(properties()) - } - - // endregion - } - - // region Internal - - private fun DDSpanBuilder.withRumContext(): DDSpanBuilder { - return if (bundleWithRum) { - val rumContext = GlobalRum.getRumContext() - withTag(LogAttributes.RUM_APPLICATION_ID, rumContext.applicationId) - .withTag(LogAttributes.RUM_SESSION_ID, rumContext.sessionId) - .withTag(LogAttributes.RUM_VIEW_ID, rumContext.viewId) - } else { - this - } - } - - // endregion - - companion object { - // the minimum closed spans required for triggering a flush and deliver - // everything to the writer - internal const val DEFAULT_PARTIAL_MIN_FLUSH = 5 - - internal const val TRACE_LOGGER_NAME = "trace" - - internal const val TRACE_ID_BIT_SIZE = 63 - - /** - * Helper method to attach a Throwable to a specific Span. - * The Throwable information (class name, message and stacktrace) will be added to the - * provided Span as standard Error Tags. - * @param span the active Span - * @param throwable the Throwable you wan to log - */ - @JvmStatic - fun logThrowable(span: Span, throwable: Throwable) { - val fieldsMap = mapOf(Fields.ERROR_OBJECT to throwable) - span.log(fieldsMap) - } - - /** - * Helper method to attach an error message to a specific Span. - * The error message will be added to the provided Span as a standard Error Tag. - * @param span the active Span - * @param message the error message you want to attach - */ - @JvmStatic - fun logErrorMessage(span: Span, message: String) { - val fieldsMap = mapOf(Fields.MESSAGE to message) - span.log(fieldsMap) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/TracingInterceptor.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/TracingInterceptor.kt deleted file mode 100644 index 13f150777c..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/TracingInterceptor.kt +++ /dev/null @@ -1,330 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing - -import com.datadog.android.DatadogInterceptor -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.utils.devLogger -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.tracing.internal.TracesFeature -import com.datadog.trace.api.DDTags -import com.datadog.trace.api.interceptor.MutableSpan -import io.opentracing.Span -import io.opentracing.SpanContext -import io.opentracing.Tracer -import io.opentracing.propagation.Format -import io.opentracing.propagation.TextMapExtractAdapter -import io.opentracing.propagation.TextMapInject -import io.opentracing.tag.Tags -import io.opentracing.util.GlobalTracer -import java.util.concurrent.atomic.AtomicReference -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response - -/** - * Provides automatic trace integration for [OkHttpClient] by way of the [Interceptor] system. - * - * This interceptor will create a [Span] around the request and fill the request information - * (url, method, status code, optional error). It will also propagate the span and trace - * information in the request header to link it with backend spans. - * - * If you use multiple Interceptors, make sure that this one is called first. - * If you also want to track network requests as RUM Resources, use the - * [DatadogInterceptor] instead, which combines the RUM and APM integrations. - * - * If you want to get more insights on the network requests (e.g.: redirections), you can also add - * this interceptor as a Network level interceptor. - * - * To use: - * ``` - * val tracedHosts = listOf("example.com", "example.eu") - * val okHttpClient = OkHttpClient.Builder() - * .addInterceptor(TracingInterceptor(tracedHosts))) - * // Optionally to get information about redirections and retries - * // .addNetworkInterceptor(new TracingInterceptor(tracedHosts)) - * .build(); - * ``` - * - * @param tracedHosts a list of all the hosts that you want to be automatically tracked - * by this interceptor. If no host is provided the interceptor won't trace any OkHttpRequest, - * nor propagate tracing information to the backend. - * @param tracedRequestListener a listener for automatically created [Span]s - * - */ -@Suppress("StringLiteralDuplication") -open class TracingInterceptor -@JvmOverloads internal constructor( - @Deprecated("hosts should be defined in the DatadogConfig.setFirstPartyHosts()") - internal val tracedHosts: List, - internal val tracedRequestListener: TracedRequestListener, - internal val firstPartyHostDetector: FirstPartyHostDetector, - internal val localTracerFactory: () -> Tracer -) : Interceptor { - - private val localTracerReference: AtomicReference = AtomicReference() - - private val hostRegex: Regex = Regex( - if (tracedHosts.isEmpty()) { - "" - } else { - tracedHosts.joinToString("|") { "^(.*\\.)*${it.replace(".", "\\.")}" } - } - ) - - init { - if (tracedHosts.isEmpty() && firstPartyHostDetector.isEmpty()) { - devLogger.w(WARNING_TRACING_NO_HOSTS) - } - } - - /** - * Creates a [TracingInterceptor] to automatically create a trace around OkHttp [Request]s. - * - * @param tracedHosts a list of all the hosts that you want to be automatically tracked - * by this interceptor. If no host is provided the interceptor won't trace any OkHttp [Request], - * nor propagate tracing information to the backend. - * @param tracedRequestListener a listener for automatically created [Span]s - */ - @JvmOverloads - @Deprecated("hosts should be defined in the DatadogConfig.setFirstPartyHosts()") - constructor( - tracedHosts: List, - tracedRequestListener: TracedRequestListener = NoOpTracedRequestListener() - ) : this( - tracedHosts, - tracedRequestListener, - CoreFeature.firstPartyHostDetector, - { AndroidTracer.Builder().build() } - ) - - /** - * Creates a [TracingInterceptor] to automatically create a trace around OkHttp [Request]s. - * - * @param tracedRequestListener a listener for automatically created [Span]s - */ - @JvmOverloads - constructor( - tracedRequestListener: TracedRequestListener = NoOpTracedRequestListener() - ) : this( - emptyList(), - tracedRequestListener, - CoreFeature.firstPartyHostDetector, - { AndroidTracer.Builder().build() } - ) - - // region Interceptor - - /** @inheritdoc */ - override fun intercept(chain: Interceptor.Chain): Response { - val tracer = resolveTracer() - val request = chain.request() - - return if (tracer == null || !isRequestTraceable(request)) { - intercept(chain, request) - } else { - interceptAndTrace(chain, request, tracer) - } - } - - // endregion - - // region TracingInterceptor - - /** - * Called whenever a span was successfully created around an OkHttp [Request]. - * The given [Span] can be updated (e.g.: add custom tags / baggage items) before it is - * finalized. - * @param request the intercepted [Request] - * @param span the [Span] created around the [Request] (or null if request is not traced) - * @param response the [Request] response (or null if an error occurred) - * @param throwable the error which occurred during the [Request] (or null) - */ - protected open fun onRequestIntercepted( - request: Request, - span: Span?, - response: Response?, - throwable: Throwable? - ) { - if (span != null) { - tracedRequestListener.onRequestIntercepted(request, span, response, throwable) - } - } - - // endregion - - // region Internal - - private fun isRequestTraceable(request: Request): Boolean { - val url = request.url() - return firstPartyHostDetector.isFirstPartyUrl(url) || - url.host().matches(hostRegex) - } - - @Suppress("TooGenericExceptionCaught", "ThrowingInternalException") - private fun interceptAndTrace( - chain: Interceptor.Chain, - request: Request, - tracer: Tracer - ): Response { - val span = buildSpan(tracer, request) - val updatedRequest = updateRequest(request, tracer, span).build() - - try { - val response = chain.proceed(updatedRequest) - handleResponse(request, response, span) - return response - } catch (e: Throwable) { - handleThrowable(request, e, span) - throw e - } - } - - @Suppress("TooGenericExceptionCaught", "ThrowingInternalException") - private fun intercept( - chain: Interceptor.Chain, - request: Request - ): Response { - try { - val response = chain.proceed(request) - onRequestIntercepted(request, null, response, null) - return response - } catch (e: Throwable) { - onRequestIntercepted(request, null, null, e) - throw e - } - } - - @Synchronized - private fun resolveTracer(): Tracer? { - return if (!TracesFeature.initialized.get()) { - devLogger.w(WARNING_TRACING_DISABLED) - null - } else if (GlobalTracer.isRegistered()) { - // clear the localTracer reference if any - localTracerReference.set(null) - GlobalTracer.get() - } else { - // we check if we already have a local tracer if not we instantiate one - resolveLocalTracer() - } - } - - private fun resolveLocalTracer(): Tracer { - if (localTracerReference.get() == null) { - // only register once - localTracerReference.compareAndSet(null, localTracerFactory()) - devLogger.w(WARNING_DEFAULT_TRACER) - } - return localTracerReference.get() - } - - private fun buildSpan(tracer: Tracer, request: Request): Span { - val parentContext = extractParentContext(tracer, request) - val url = request.url().toString() - - val span = tracer.buildSpan(SPAN_NAME) - .asChildOf(parentContext) - .start() - - (span as? MutableSpan)?.resourceName = url - span.setTag(Tags.HTTP_URL.key, url) - span.setTag(Tags.HTTP_METHOD.key, request.method()) - - return span - } - - private fun extractParentContext(tracer: Tracer, request: Request): SpanContext? { - val tagContext = request.tag(Span::class.java)?.context() - - val headerContext = tracer.extract( - Format.Builtin.TEXT_MAP_EXTRACT, - TextMapExtractAdapter( - request.headers().toMultimap() - .map { it.key to it.value.joinToString(";") } - .toMap() - ) - ) - - return headerContext ?: tagContext - } - - private fun updateRequest( - request: Request, - tracer: Tracer, - span: Span - ): Request.Builder { - val tracedRequestBuilder = request.newBuilder() - - tracer.inject( - span.context(), - Format.Builtin.TEXT_MAP_INJECT, - TextMapInject { key, value -> - tracedRequestBuilder.addHeader(key, value) - } - ) - - return tracedRequestBuilder - } - - private fun handleResponse( - request: Request, - response: Response, - span: Span? - ) { - val statusCode = response.code() - span?.setTag(Tags.HTTP_STATUS.key, statusCode) - if (statusCode in 400..499) { - (span as? MutableSpan)?.setError(true) - } - if (statusCode == 404) { - (span as? MutableSpan)?.setResourceName(RESOURCE_NAME_404) - } - onRequestIntercepted(request, span, response, null) - span?.finish() - } - - private fun handleThrowable( - request: Request, - throwable: Throwable, - span: Span - ) { - (span as? MutableSpan)?.setError(true) - span.setTag(DDTags.ERROR_MSG, throwable.message) - span.setTag(DDTags.ERROR_TYPE, throwable.javaClass.name) - span.setTag(DDTags.ERROR_STACK, throwable.loggableStackTrace()) - onRequestIntercepted(request, span, null, throwable) - span.finish() - } - - // endregion - - companion object { - internal const val SPAN_NAME = "okhttp.request" - - internal const val RESOURCE_NAME_404 = "404" - - internal const val HEADER_CT = "Content-Type" - - internal const val WARNING_TRACING_NO_HOSTS = - "You added a TracingInterceptor to your OkHttpClient, " + - "but you did not specify any first party hosts. " + - "Your requests won't be traced.\n" + - "To set a list of known hosts, you can use the " + - "DatadogConfig.Builder::setFirstPartyHosts() method." - internal const val WARNING_TRACING_DISABLED = - "You added a TracingInterceptor to your OkHttpClient, " + - "but you did not enable the TracesFeature. " + - "Your requests won't be traced." - internal const val WARNING_DEFAULT_TRACER = - "You added a TracingInterceptor to your OkHttpClient, " + - "but you didn't register any Tracer. " + - "We automatically created a local tracer for you." - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/TracesFeature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/TracesFeature.kt deleted file mode 100644 index 7a5b8a82e6..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/TracesFeature.kt +++ /dev/null @@ -1,139 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing.internal - -import android.content.Context -import com.datadog.android.DatadogConfig -import com.datadog.android.DatadogEndpoint -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.SdkFeature -import com.datadog.android.core.internal.data.upload.DataUploadScheduler -import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler -import com.datadog.android.core.internal.data.upload.UploadScheduler -import com.datadog.android.core.internal.domain.NoOpPersistenceStrategy -import com.datadog.android.core.internal.domain.PersistenceStrategy -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.NoOpDataUploader -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.core.internal.time.TimeProvider -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.plugin.DatadogPluginConfig -import com.datadog.android.tracing.internal.domain.TracingFileStrategy -import com.datadog.android.tracing.internal.net.TracesOkHttpUploader -import com.datadog.opentracing.DDSpan -import java.util.concurrent.ExecutorService -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.atomic.AtomicBoolean -import okhttp3.OkHttpClient - -internal object TracesFeature : SdkFeature() { - - internal val initialized = AtomicBoolean(false) - - internal var clientToken: String = "" - internal var endpointUrl: String = DatadogEndpoint.TRACES_US - - internal var persistenceStrategy: PersistenceStrategy = NoOpPersistenceStrategy() - internal var uploader: DataUploader = NoOpDataUploader() - internal var dataUploadScheduler: UploadScheduler = NoOpUploadScheduler() - - @Suppress("LongParameterList") - fun initialize( - appContext: Context, - config: DatadogConfig.FeatureConfig, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - userInfoProvider: UserInfoProvider, - systemInfoProvider: SystemInfoProvider, - timeProvider: TimeProvider, - dataUploadThreadPoolExecutor: ScheduledThreadPoolExecutor, - dataPersistenceExecutor: ExecutorService, - trackingConsentProvider: ConsentProvider - ) { - if (initialized.get()) { - return - } - - clientToken = config.clientToken - endpointUrl = config.endpointUrl - - persistenceStrategy = TracingFileStrategy( - appContext, - timeProvider = timeProvider, - networkInfoProvider = networkInfoProvider, - userInfoProvider = userInfoProvider, - envName = config.envName, - dataPersistenceExecutorService = dataPersistenceExecutor, - trackingConsentProvider = trackingConsentProvider - ) - setupUploader( - endpointUrl, - okHttpClient, - networkInfoProvider, - systemInfoProvider, - dataUploadThreadPoolExecutor - ) - - registerPlugins( - config.plugins, - DatadogPluginConfig.TracingPluginConfig( - appContext, - config.envName, - CoreFeature.serviceName, - trackingConsentProvider.getConsent() - ), - trackingConsentProvider - ) - initialized.set(true) - } - - fun clearAllData() { - persistenceStrategy.clearAllData() - } - - fun stop() { - if (initialized.get()) { - unregisterPlugins() - dataUploadScheduler.stopScheduling() - persistenceStrategy = NoOpPersistenceStrategy() - dataUploadScheduler = NoOpUploadScheduler() - clientToken = "" - endpointUrl = DatadogEndpoint.TRACES_US - initialized.set(false) - } - } - - // region Internal - - private fun setupUploader( - endpointUrl: String, - okHttpClient: OkHttpClient, - networkInfoProvider: NetworkInfoProvider, - systemInfoProvider: SystemInfoProvider, - dataUploadThreadPoolExecutor: ScheduledThreadPoolExecutor - ) { - uploader = TracesOkHttpUploader(endpointUrl, clientToken, okHttpClient) - - dataUploadScheduler = if (CoreFeature.isMainProcess) { - uploader = TracesOkHttpUploader(endpointUrl, clientToken, okHttpClient) - DataUploadScheduler( - persistenceStrategy.getReader(), - uploader, - networkInfoProvider, - systemInfoProvider, - dataUploadThreadPoolExecutor - ) - } else { - NoOpUploadScheduler() - } - dataUploadScheduler.startScheduling() - } - - // endregion -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/data/TraceWriter.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/data/TraceWriter.kt deleted file mode 100644 index cac4ffa9ec..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/data/TraceWriter.kt +++ /dev/null @@ -1,92 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing.internal.data - -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.opentracing.DDSpan -import com.datadog.trace.api.DDTags -import com.datadog.trace.common.writer.Writer -import java.util.Locale - -internal class TraceWriter( - val writer: com.datadog.android.core.internal.data.Writer -) : Writer { - - // region Writer - override fun start() { - // NO - OP - } - - override fun write(trace: MutableList?) { - trace?.let { - it.filter { it.isError }.forEach { span -> - sendRumErrorEvent(span) - } - writer.write(it) - } - } - - override fun close() { - // NO - OP - } - - override fun incrementTraceCount() { - // NO - OP - } - - // endregion - - // region Internals - - private fun sendRumErrorEvent(span: DDSpan) { - (GlobalRum.get() as? AdvancedRumMonitor)?.addErrorWithStacktrace( - spanErrorMessage(span), - RumErrorSource.SOURCE, - span.tags[DDTags.ERROR_STACK]?.toString(), - emptyMap() - ) - } - - private fun spanErrorMessage(span: DDSpan): String { - val errorType = span.tags[DDTags.ERROR_TYPE] - val errorMessage = span.tags[DDTags.ERROR_MSG] - return when { - errorMessage != null && errorType != null -> - SPAN_ERROR_WITH_TYPE_AND_MESSAGE_FORMAT.format( - Locale.US, - span.operationName, - errorType, - errorMessage - ) - errorType != null -> - SPAN_ERROR_WITH_TYPE_FORMAT.format( - Locale.US, - span.operationName, - errorType - ) - errorMessage != null -> - SPAN_ERROR_WITH_MESSAGE_FORMAT.format( - Locale.US, - span.operationName, - errorMessage - ) - else -> SPAN_ERROR_FORMAT.format(Locale.US, span.operationName) - } - } - - // endregion - - companion object { - internal const val SPAN_ERROR_FORMAT: String = "Span error (%s)" - internal const val SPAN_ERROR_WITH_TYPE_AND_MESSAGE_FORMAT: String = - "Span error (%s): %s | %s " - internal const val SPAN_ERROR_WITH_TYPE_FORMAT: String = "Span error (%s): %s" - internal const val SPAN_ERROR_WITH_MESSAGE_FORMAT: String = "Span error (%s): %s" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/domain/SpanSerializer.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/domain/SpanSerializer.kt deleted file mode 100644 index 8b1521e627..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/domain/SpanSerializer.kt +++ /dev/null @@ -1,210 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing.internal.domain - -import com.datadog.android.BuildConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.constraints.DataConstraints -import com.datadog.android.core.internal.constraints.DatadogDataConstraints -import com.datadog.android.core.internal.domain.Serializer -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.time.TimeProvider -import com.datadog.android.core.internal.utils.NULL_MAP_VALUE -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.opentracing.DDSpan -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.google.gson.JsonPrimitive -import java.util.Date - -internal class SpanSerializer( - private val timeProvider: TimeProvider, - private val networkInfoProvider: NetworkInfoProvider, - private val userInfoProvider: UserInfoProvider, - private val envName: String, - private val dataConstraints: DataConstraints = DatadogDataConstraints() -) : Serializer { - - // region Serializer - - override fun serialize(model: DDSpan): String { - val span = serializeSpan(model) - val spans = JsonArray(1) - spans.add(span) - - val jsonObject = JsonObject() - jsonObject.add(TAG_SPANS, spans) - jsonObject.addProperty(TAG_ENV, envName) - - return jsonObject.toString() - } - - // endregion - - // region Internal - - private fun serializeSpan(model: DDSpan): JsonObject { - - val serverOffset = timeProvider.getServerOffsetNanos() - val jsonObject = JsonObject() - // it is safe to convert BigInteger IDs to Long as they are parsed as Long on the backend - jsonObject.addProperty(TAG_TRACE_ID, model.traceId.toLong().toString(16)) - jsonObject.addProperty(TAG_SPAN_ID, model.spanId.toLong().toString(16)) - jsonObject.addProperty(TAG_PARENT_ID, model.parentId.toLong().toString(16)) - jsonObject.addProperty(TAG_RESOURCE, model.resourceName) - jsonObject.addProperty(TAG_OPERATION_NAME, model.operationName) - jsonObject.addProperty(TAG_SERVICE_NAME, model.serviceName) - jsonObject.addProperty(TAG_DURATION, model.durationNano) - jsonObject.addProperty(TAG_START_TIMESTAMP, model.startTime + serverOffset) - jsonObject.addProperty(TAG_ERROR, if (model.isError) 1 else 0) - jsonObject.addProperty(TAG_TYPE, TYPE_CUSTOM) - addMeta(jsonObject, model) - addMetrics(jsonObject, model) - return jsonObject - } - - private fun addMeta(jsonObject: JsonObject, model: DDSpan) { - val metaObject = JsonObject() - model.meta.forEach { - metaObject.addProperty(it.key, it.value) - } - - metaObject.addProperty(TAG_DD_SOURCE, DD_SOURCE_ANDROID) - metaObject.addProperty(TAG_SPAN_KIND, KIND_CLIENT) - metaObject.addProperty(TAG_TRACER_VERSION, BuildConfig.VERSION_NAME) - metaObject.addProperty(TAG_APPLICATION_VERSION, CoreFeature.packageVersion) - - addLogNetworkInfo(networkInfoProvider.getLatestNetworkInfo(), metaObject) - addLogUserInfo(userInfoProvider.getUserInfo(), metaObject) - - jsonObject.add(TAG_META, metaObject) - } - - private fun addMetrics(jsonObject: JsonObject, model: DDSpan) { - val metricsObject = JsonObject() - model.metrics.forEach { - metricsObject.addProperty(it.key, it.value) - } - if (model.parentId.toLong() == 0L) { - // mark this span as top level - metricsObject.addProperty(TAG_METRICS_TOP_LEVEL, 1) - } - jsonObject.add(TAG_METRICS, metricsObject) - } - - private fun addLogNetworkInfo( - networkInfo: NetworkInfo?, - jsonLog: JsonObject - ) { - if (networkInfo != null) { - jsonLog.addProperty( - LogAttributes.NETWORK_CONNECTIVITY, - networkInfo.connectivity.serialized - ) - if (!networkInfo.carrierName.isNullOrBlank()) { - jsonLog.addProperty( - LogAttributes.NETWORK_CARRIER_NAME, - networkInfo.carrierName - ) - } - if (networkInfo.carrierId >= 0) { - jsonLog.addProperty( - LogAttributes.NETWORK_CARRIER_ID, - networkInfo.carrierId.toString() - ) - } - if (networkInfo.upKbps >= 0) { - jsonLog.addProperty( - LogAttributes.NETWORK_UP_KBPS, - networkInfo.upKbps.toString() - ) - } - if (networkInfo.downKbps >= 0) { - jsonLog.addProperty( - LogAttributes.NETWORK_DOWN_KBPS, - networkInfo.downKbps.toString() - ) - } - if (networkInfo.strength > Int.MIN_VALUE) { - jsonLog.addProperty( - LogAttributes.NETWORK_SIGNAL_STRENGTH, - networkInfo.strength.toString() - ) - } - } - } - - private fun addLogUserInfo(userInfo: UserInfo, jsonLog: JsonObject) { - if (!userInfo.id.isNullOrEmpty()) { - jsonLog.addProperty(LogAttributes.USR_ID, userInfo.id) - } - if (!userInfo.name.isNullOrEmpty()) { - jsonLog.addProperty(LogAttributes.USR_NAME, userInfo.name) - } - if (!userInfo.email.isNullOrEmpty()) { - jsonLog.addProperty(LogAttributes.USR_EMAIL, userInfo.email) - } - // add extra attributes - dataConstraints.validateAttributes( - userInfo.extraInfo, - keyPrefix = LogAttributes.USR_ATTRIBUTES_GROUP, - attributesGroupName = USER_EXTRA_GROUP_VERBOSE_NAME - ).forEach { - val key = "${LogAttributes.USR_ATTRIBUTES_GROUP}.${it.key}" - toMetaString(it.value)?.apply { - jsonLog.addProperty(key, this) - } - } - } - - private fun toMetaString(element: Any?): String? { - return when (element) { - NULL_MAP_VALUE -> null - null -> null - is Date -> element.time.toString() - is JsonPrimitive -> element.asString - else -> element.toString() - } - } - - // endregion - - companion object { - - internal const val TYPE_CUSTOM = "custom" - internal const val DD_SOURCE_ANDROID = "android" - internal const val KIND_CLIENT = "client" - - // PAYLOAD TAGS - internal const val TAG_SPANS = "spans" - internal const val TAG_ENV = "env" - - // SPAN TAGS - internal const val TAG_START_TIMESTAMP = "start" - internal const val TAG_DURATION = "duration" - internal const val TAG_SERVICE_NAME = "service" - internal const val TAG_APPLICATION_VERSION = "version" - internal const val TAG_TRACE_ID = "trace_id" - internal const val TAG_SPAN_ID = "span_id" - internal const val TAG_PARENT_ID = "parent_id" - internal const val TAG_RESOURCE = "resource" - internal const val TAG_OPERATION_NAME = "name" - internal const val TAG_ERROR = "error" - internal const val TAG_TYPE = "type" - internal const val TAG_META = "meta" - internal const val TAG_METRICS = "metrics" - internal const val TAG_METRICS_TOP_LEVEL = "_top_level" - internal const val TAG_DD_SOURCE = "_dd.source" - internal const val TAG_SPAN_KIND = "span.kind" - internal const val TAG_TRACER_VERSION = "tracer.version" - - internal const val USER_EXTRA_GROUP_VERBOSE_NAME = "user extra information" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/domain/TracingFileStrategy.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/domain/TracingFileStrategy.kt deleted file mode 100644 index 2c19695edd..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/domain/TracingFileStrategy.kt +++ /dev/null @@ -1,48 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing.internal.domain - -import android.content.Context -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.FilePersistenceStrategy -import com.datadog.android.core.internal.domain.PayloadDecoration -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.time.TimeProvider -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.opentracing.DDSpan -import java.io.File -import java.util.concurrent.ExecutorService - -internal class TracingFileStrategy( - context: Context, - filePersistenceConfig: FilePersistenceConfig = - FilePersistenceConfig(recentDelayMs = MAX_DELAY_BETWEEN_SPANS_MS), - timeProvider: TimeProvider, - networkInfoProvider: NetworkInfoProvider, - userInfoProvider: UserInfoProvider, - envName: String = "", - dataPersistenceExecutorService: ExecutorService, - trackingConsentProvider: ConsentProvider -) : FilePersistenceStrategy( - File(context.filesDir, INTERMEDIATE_DATA_FOLDER), - File(context.filesDir, AUTHORIZED_FOLDER), - SpanSerializer(timeProvider, networkInfoProvider, userInfoProvider, envName), - dataPersistenceExecutorService, - filePersistenceConfig, - PayloadDecoration.NEW_LINE_DECORATION, - trackingConsentProvider -) { - companion object { - internal const val VERSION = 1 - internal const val ROOT = "dd-tracing" - internal const val INTERMEDIATE_DATA_FOLDER = - "$ROOT-pending-v$VERSION" - internal const val AUTHORIZED_FOLDER = "$ROOT-v$VERSION" - internal const val MAX_DELAY_BETWEEN_SPANS_MS = 5000L - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/handlers/AndroidSpanLogsHandler.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/handlers/AndroidSpanLogsHandler.kt deleted file mode 100644 index a86b81be76..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/handlers/AndroidSpanLogsHandler.kt +++ /dev/null @@ -1,104 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing.internal.handlers - -import android.util.Log -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.Logger -import com.datadog.opentracing.DDSpan -import com.datadog.opentracing.LogHandler -import com.datadog.trace.api.DDTags -import io.opentracing.log.Fields -import java.util.concurrent.TimeUnit - -internal class AndroidSpanLogsHandler( - val logger: Logger -) : LogHandler { - - // region Span - - override fun log(event: String, span: DDSpan) { - logFields( - span, - mutableMapOf(Fields.EVENT to event), - null - ) - } - - override fun log(timestampMicroseconds: Long, event: String, span: DDSpan) { - logFields( - span, - mutableMapOf(Fields.EVENT to event), - timestampMicroseconds - ) - } - - override fun log(fields: Map, span: DDSpan) { - val mutableMap = fields.toMutableMap() - extractError(mutableMap, span) - logFields(span, mutableMap) - } - - override fun log(timestampMicroseconds: Long, fields: Map, span: DDSpan) { - val mutableMap = fields.toMutableMap() - extractError(mutableMap, span) - logFields(span, mutableMap, timestampMicroseconds) - } - - // endregion - - // region Internal - - private fun toMilliseconds(timestampMicroseconds: Long?): Long? { - return timestampMicroseconds?.let { TimeUnit.MILLISECONDS.toSeconds(it) } - } - - private fun logFields( - span: DDSpan, - fields: MutableMap, - timestampMicroseconds: Long? = null - ) { - val message = fields.remove(Fields.MESSAGE)?.toString() ?: DEFAULT_EVENT_MESSAGE - fields[LogAttributes.DD_TRACE_ID] = span.traceId.toString() - fields[LogAttributes.DD_SPAN_ID] = span.spanId.toString() - logger.internalLog( - Log.VERBOSE, - message, - null, - fields, - toMilliseconds(timestampMicroseconds) - ) - } - - private fun extractError( - map: MutableMap, - span: DDSpan - ) { - val throwable = map.remove(Fields.ERROR_OBJECT) as? Throwable - val kind = map.remove(Fields.ERROR_KIND) - val errorType = kind?.toString() ?: throwable?.javaClass?.name - - if (errorType != null) { - val stackField = map.remove(Fields.STACK) - val msgField = map.remove(Fields.MESSAGE) - val stack = stackField?.toString() ?: throwable?.loggableStackTrace() - val message = msgField?.toString() ?: throwable?.message - - span.setError(true) - span.setTag(DDTags.ERROR_TYPE, errorType) - span.setTag(DDTags.ERROR_MSG, message) - span.setTag(DDTags.ERROR_STACK, stack) - } - } - - // endregion - - companion object { - internal const val DEFAULT_EVENT_MESSAGE = "Span event" - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/net/TracesOkHttpUploader.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/net/TracesOkHttpUploader.kt deleted file mode 100644 index 386532c016..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/net/TracesOkHttpUploader.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing.internal.net - -import com.datadog.android.core.internal.net.DataOkHttpUploader -import java.util.Locale -import okhttp3.OkHttpClient - -internal open class TracesOkHttpUploader( - endpoint: String, - private val token: String, - client: OkHttpClient -) : DataOkHttpUploader(buildUrl(endpoint, token), client, CONTENT_TYPE_TEXT_UTF8) { - - // region DataOkHttpUploader - - override fun setEndpoint(endpoint: String) { - super.setEndpoint(buildUrl(endpoint, token)) - } - - override fun buildQueryParams(): Map { - return emptyMap() - } - - // endregion - - companion object { - internal const val UPLOAD_URL = "%s/v1/input/%s" - - private fun buildUrl(endpoint: String, token: String): String { - return String.format( - Locale.US, - UPLOAD_URL, - endpoint, - token - ) - } - } -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/utils/TracerExtensions.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/utils/TracerExtensions.kt deleted file mode 100644 index e00d457f74..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/tracing/internal/utils/TracerExtensions.kt +++ /dev/null @@ -1,25 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.tracing.internal.utils - -import com.datadog.opentracing.DDSpan -import io.opentracing.Span -import io.opentracing.Tracer - -internal fun Tracer.traceId(): String? { - val activeSpan: Span? = activeSpan() - return if (activeSpan is DDSpan) { - activeSpan.traceId.toString() - } else null -} - -internal fun Tracer.spanId(): String? { - val activeSpan: Span? = activeSpan() - return if (activeSpan is DDSpan) { - activeSpan.spanId.toString() - } else null -} diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/tools/annotation/NoOpImplementation.kt b/dd-sdk-android/src/main/kotlin/com/datadog/tools/annotation/NoOpImplementation.kt deleted file mode 100644 index 71eb8af057..0000000000 --- a/dd-sdk-android/src/main/kotlin/com/datadog/tools/annotation/NoOpImplementation.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.tools.annotation - -/** - * Adding this annotation on an interface will generate a No-Op implementation class. - */ -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.BINARY) -annotation class NoOpImplementation diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogConfigBuilderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogConfigBuilderTest.kt deleted file mode 100644 index fcc53fea04..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogConfigBuilderTest.kt +++ /dev/null @@ -1,894 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import android.os.Build -import android.util.Log -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.Feature -import com.datadog.android.rum.assertj.RumConfigAssert.Companion.assertThat -import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy -import com.datadog.android.rum.tracking.ViewAttributesProvider -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockDevLogHandler -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.annotation.FloatForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.UUID -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings() -@ForgeConfiguration(value = Configurator::class) -internal class DatadogConfigBuilderTest { - - lateinit var testedBuilder: DatadogConfig.Builder - - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeClientToken: String - - @StringForgery - lateinit var fakeEnvName: String - - @Forgery - lateinit var fakeApplicationId: UUID - - lateinit var mockDevLogHandler: LogHandler - - // region Unit Tests - - @BeforeEach - fun `set up`() { - mockDevLogHandler = mockDevLogHandler() - testedBuilder = DatadogConfig.Builder(fakeClientToken, fakeEnvName, fakeApplicationId) - } - - @Test - fun `𝕄 use sensible defaults 𝕎 build() {no applicationId}`() { - // Given - val builder = DatadogConfig.Builder(fakeClientToken, fakeEnvName) - - // When - val config = builder.build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - UUID(0, 0), - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - UUID(0, 0), - DatadogEndpoint.TRACES_US, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - UUID(0, 0), - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.rumConfig) - .isNull() - } - - @Test - fun `𝕄 use sensible defaults 𝕎 build() {String applicationId}`() { - // Given - val builder = DatadogConfig.Builder( - fakeClientToken, - fakeEnvName, - fakeApplicationId.toString() - ) - - // When - val config = builder.build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_US, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_US, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 use sensible defaults 𝕎 build() {UUID applicationId}`() { - // When - val config = testedBuilder.build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_US, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_US, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 build config with serviceName 𝕎 setServiceName() and build()`( - @StringForgery serviceName: String - ) { - // When - val config = testedBuilder - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .setServiceName(serviceName) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - envName = fakeEnvName, - serviceName = serviceName - ) - ) - } - - @Test - fun `𝕄 build config with envName 𝕎 setEnvironmentName() and build()`( - @StringForgery envName: String - ) { - // When - val config = testedBuilder - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .setEnvironmentName(envName) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = envName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - envName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_US, - envName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - envName - ) - ) - - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_US, - envName = envName - ) - ) - } - - @Test - fun `𝕄 build config with all features disabled 𝕎 setXXXEnabled(false) and build()`() { - // When - val config = testedBuilder - .setLogsEnabled(false) - .setTracesEnabled(false) - .setCrashReportsEnabled(false) - .setRumEnabled(false) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig).isNull() - assertThat(config.tracesConfig).isNull() - assertThat(config.crashReportConfig).isNull() - assertThat(config.rumConfig).isNull() - } - - @Test - fun `𝕄 build config with US endpoints 𝕎 useUSEndpoints() and build()`() { - // When - val config = testedBuilder - .useUSEndpoints() - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_US, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_US, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 build config with EU endpoints 𝕎 useEUEndpoints() and build()`() { - // When - val config = testedBuilder - .useEUEndpoints() - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_EU, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_EU, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_EU, - fakeEnvName - ) - ) - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_EU, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 build config with GOV endpoints 𝕎 useGovEndpoints() and build()`() { - // When - val config = testedBuilder - .useGovEndpoints() - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_GOV, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_GOV, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_GOV, - fakeEnvName - ) - ) - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_GOV, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 build config with custom endpoints 𝕎 useCustomXXXEndpoint() and build()`( - @RegexForgery("https://[a-z]+\\.com") logsUrl: String, - @RegexForgery("https://[a-z]+\\.com") tracesUrl: String, - @RegexForgery("https://[a-z]+\\.com") crashReportsUrl: String, - @RegexForgery("https://[a-z]+\\.com") rumUrl: String - ) { - // When - val config = testedBuilder - .useCustomLogsEndpoint(logsUrl) - .useCustomTracesEndpoint(tracesUrl) - .useCustomCrashReportsEndpoint(crashReportsUrl) - .useCustomRumEndpoint(rumUrl) - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - logsUrl, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - tracesUrl, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - crashReportsUrl, - fakeEnvName - ) - ) - - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - rumUrl, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 build config with custom cleartext endpoints 𝕎 useCustomXXXEndpoint() and build()`( - @RegexForgery("http://[a-z]+\\.com") logsUrl: String, - @RegexForgery("http://[a-z]+\\.com") tracesUrl: String, - @RegexForgery("http://[a-z]+\\.com") crashReportsUrl: String, - @RegexForgery("http://[a-z]+\\.com") rumUrl: String - ) { - // When - val config = testedBuilder - .useCustomLogsEndpoint(logsUrl) - .useCustomTracesEndpoint(tracesUrl) - .useCustomCrashReportsEndpoint(crashReportsUrl) - .useCustomRumEndpoint(rumUrl) - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = true, - envName = fakeEnvName - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - logsUrl, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - tracesUrl, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - crashReportsUrl, - fakeEnvName - ) - ) - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - rumUrl, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 build config with first party hosts 𝕎 setFirstPartyHosts() and build()`( - @StringForgery(regex = "([a-zA-Z0-9]{3,9}\\.){1,4}[a-z]{3}") hosts: List - ) { - // When - val config = testedBuilder - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .setFirstPartyHosts(hosts) - .build() - - // Then - assertThat(config.coreConfig) - .isEqualTo( - DatadogConfig.CoreConfig( - needsClearTextHttp = false, - envName = fakeEnvName, - hosts = hosts - ) - ) - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_US, - fakeEnvName - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName - ) - ) - - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_US, - fakeEnvName - ) - ) - } - - @Test - fun `𝕄 build RUM config with gestures enabled 𝕎 trackInteractions() and build()`( - @RegexForgery("http://[a-z]+\\.com") rumUrl: String, - @IntForgery(0, 10) attributesCount: Int - ) { - // Given - val touchTargetExtraAttributesProviders = Array(attributesCount) { - mock() - } - - // When - val config = testedBuilder - .useCustomRumEndpoint(rumUrl) - .trackInteractions(touchTargetExtraAttributesProviders) - .build() - - // Then - val rumConfig: DatadogConfig.RumConfig? = config.rumConfig - assertThat(rumConfig).isNotNull() - assertThat(rumConfig!!) - .hasClientToken(fakeClientToken) - .hasApplicationId(fakeApplicationId) - .hasEndpointUrl(rumUrl) - .hasEnvName(fakeEnvName) - .hasGesturesTrackingStrategy(touchTargetExtraAttributesProviders) - .doesNotHaveViewTrackingStrategy() - } - - @TestTargetApi(value = Build.VERSION_CODES.Q) - @Test - fun `𝕄 build RUM config with gestures enabled 𝕎 trackInteractions() and build() {Android Q}`( - @RegexForgery("http://[a-z]+\\.com") rumUrl: String, - @IntForgery(0, 10) attributesCount: Int - ) { - // Given - val touchTargetExtraAttributesProviders = Array(attributesCount) { - mock() - } - - // When - val config = testedBuilder - .useCustomRumEndpoint(rumUrl) - .trackInteractions(touchTargetExtraAttributesProviders) - .build() - val rumConfig: DatadogConfig.RumConfig? = config.rumConfig - assertThat(rumConfig).isNotNull() - assertThat(rumConfig!!) - .hasClientToken(fakeClientToken) - .hasApplicationId(fakeApplicationId) - .hasEndpointUrl(rumUrl) - .hasEnvName(fakeEnvName) - .hasGesturesTrackingStrategyApi29(touchTargetExtraAttributesProviders) - .doesNotHaveViewTrackingStrategy() - } - - @Test - fun `𝕄 build RUM config with view strategy enabled 𝕎 useViewTrackingStrategy() and build()`( - @RegexForgery("http://[a-z]+\\.com") rumUrl: String - ) { - // Given - val strategy = ActivityViewTrackingStrategy(true) - - // When - val config = testedBuilder - .useCustomRumEndpoint(rumUrl) - .useViewTrackingStrategy(strategy) - .build() - - // Then - val rumConfig: DatadogConfig.RumConfig? = config.rumConfig - assertThat(rumConfig).isNotNull() - assertThat(rumConfig!!) - .hasClientToken(fakeClientToken) - .hasApplicationId(fakeApplicationId) - .hasEndpointUrl(rumUrl) - .hasEnvName(fakeEnvName) - .doesNotHaveGesturesTrackingStrategy() - .hasViewTrackingStrategy(strategy) - } - - @Test - fun `𝕄 build RUM config with sampling rate 𝕎 sampleRumSessions() and build()`( - @FloatForgery(min = 0f, max = 100f) sampling: Float - ) { - // When - val config = testedBuilder - .setRumEnabled(true) - .sampleRumSessions(sampling) - .build() - - // Then - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_US, - fakeEnvName, - samplingRate = sampling - ) - ) - } - - @Test - fun `𝕄 build config with plugin 𝕎 addPlugin() and build()`() { - // Given - val logsPlugin: DatadogPlugin = mock() - val tracesPlugin: DatadogPlugin = mock() - val rumPlugin: DatadogPlugin = mock() - val crashPlugin: DatadogPlugin = mock() - - // When - val config = testedBuilder - .setLogsEnabled(true) - .setTracesEnabled(true) - .setCrashReportsEnabled(true) - .setRumEnabled(true) - .addPlugin(logsPlugin, Feature.LOG) - .addPlugin(tracesPlugin, Feature.TRACE) - .addPlugin(rumPlugin, Feature.RUM) - .addPlugin(crashPlugin, Feature.CRASH) - .build() - - // Then - assertThat(config.logsConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName, - plugins = listOf(logsPlugin) - ) - ) - assertThat(config.tracesConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.TRACES_US, - fakeEnvName, - plugins = listOf(tracesPlugin) - ) - ) - assertThat(config.crashReportConfig) - .isEqualTo( - DatadogConfig.FeatureConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.LOGS_US, - fakeEnvName, - plugins = listOf(crashPlugin) - - ) - ) - assertThat(config.rumConfig) - .isEqualTo( - DatadogConfig.RumConfig( - fakeClientToken, - fakeApplicationId, - DatadogEndpoint.RUM_US, - fakeEnvName, - plugins = listOf(rumPlugin) - - ) - ) - } - - @Test - fun `M do nothing W enabling RUM { APP_ID not provided }`() { - // WHEN - val config = - DatadogConfig.Builder(fakeClientToken, fakeEnvName) - .setRumEnabled(true) - .build() - // THEN - assertThat(config.rumConfig).isNull() - verify(mockDevLogHandler).handleLog( - Log.WARN, - DatadogConfig.Builder.RUM_NOT_INITIALISED_WARNING_MESSAGE - ) - } - - @Test - fun `M not send any warning W disabling RUM { APP_ID not provided }`() { - // WHEN - val config = - DatadogConfig.Builder(fakeClientToken, fakeEnvName) - .setRumEnabled(false) - .build() - // THEN - assertThat(config.rumConfig).isNull() - verifyZeroInteractions(mockDevLogHandler) - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogEventListenerFactoryTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogEventListenerFactoryTest.kt deleted file mode 100644 index e04379d931..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogEventListenerFactoryTest.kt +++ /dev/null @@ -1,67 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import com.datadog.android.core.internal.net.identifyRequest -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import okhttp3.Call -import okhttp3.Request -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -class DatadogEventListenerFactoryTest { - - lateinit var testedFactory: DatadogEventListener.Factory - - @Mock - lateinit var mockCall: Call - - @RegexForgery("[a-z]+\\.[a-z]{3}") - lateinit var fakeDomain: String - - lateinit var fakeRequest: Request - - @BeforeEach - fun `set up`() { - - fakeRequest = Request.Builder() - .get().url("https://$fakedomain/") - .build() - - whenever(mockCall.request()) doReturn fakeRequest - - testedFactory = DatadogEventListener.Factory() - } - - @Test - fun `𝕄 create event listener 𝕎 create()`() { - // When - val result = testedFactory.create(mockCall) - - // Then - check(result is DatadogEventListener) - assertThat(result.key).isEqualTo(identifyRequest(fakeRequest)) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogEventListenerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogEventListenerTest.kt deleted file mode 100644 index 9b4c923b1f..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogEventListenerTest.kt +++ /dev/null @@ -1,352 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.IOException -import java.net.InetSocketAddress -import java.net.Proxy -import java.util.concurrent.TimeUnit -import okhttp3.Call -import okhttp3.EventListener -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.Response -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogEventListenerTest { - - lateinit var testedListener: EventListener - - @Mock - lateinit var mockMonitor: AdvancedRumMonitor - - @Mock - lateinit var mockCall: Call - - @StringForgery(type = StringForgeryType.ASCII) - lateinit var fakeKey: String - - @RegexForgery("[a-z]+\\.[a-z]{3}") - lateinit var fakeDomain: String - - @LongForgery(min = 1L) - var fakeByteCount: Long = 1L - - lateinit var fakeRequest: Request - lateinit var fakeResponse: Response - - @BeforeEach - fun `set up`() { - fakeRequest = Request.Builder().get().url("https://$fakedomain/").build() - fakeResponse = Response.Builder() - .request(fakeRequest) - .protocol(Protocol.HTTP_2) - .code(200) - .message("lorem ipsum dolor sit amet…") - .build() - - GlobalRum.monitor = mockMonitor - GlobalRum.isRegistered.set(true) - - testedListener = DatadogEventListener(fakeKey) - } - - @AfterEach - fun `tear down`() { - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.isRegistered.set(false) - } - - @Test - fun `𝕄 call waitForTiming() 𝕎 callStart()`() { - // When - testedListener.callStart(mockCall) - - // Then - verify(mockMonitor).waitForResourceTiming(fakeKey) - verifyNoMoreInteractions(mockMonitor, mockCall) - } - - @Test - fun `𝕄 send timing info 𝕎 responseHeadersEnd() for failing request`( - @IntForgery(400, 600) statusCode: Int - ) { - // Given - fakeResponse = Response.Builder() - .request(fakeRequest) - .protocol(Protocol.HTTP_2) - .code(statusCode) - .message("lorem ipsum dolor sit amet…") - .build() - - // When - testedListener.callStart(mockCall) - Thread.sleep(10) - testedListener.dnsStart(mockCall, fakeDomain) - Thread.sleep(10) - testedListener.dnsEnd(mockCall, fakeDomain, emptyList()) - Thread.sleep(10) - testedListener.connectStart(mockCall, InetSocketAddress(0), Proxy.NO_PROXY) - Thread.sleep(10) - testedListener.secureConnectStart(mockCall) - Thread.sleep(10) - testedListener.secureConnectEnd(mockCall, null) - Thread.sleep(10) - testedListener.connectEnd(mockCall, InetSocketAddress(0), Proxy.NO_PROXY, Protocol.HTTP_2) - Thread.sleep(10) - testedListener.responseHeadersStart(mockCall) - Thread.sleep(10) - testedListener.responseHeadersEnd(mockCall, fakeResponse) - - // Then - argumentCaptor { - inOrder(mockMonitor, mockCall) { - verify(mockMonitor).waitForResourceTiming(fakeKey) - verify(mockMonitor).addResourceTiming(eq(fakeKey), capture()) - verifyNoMoreInteractions() - } - - val timing = firstValue - assertThat(timing.dnsStart).isGreaterThan(0L).isLessThan(TWENTY_MILLIS_NS) - assertThat(timing.dnsDuration).isGreaterThan(0L) - assertThat(timing.connectStart).isGreaterThan(0L) - assertThat(timing.connectDuration).isGreaterThan(0L) - assertThat(timing.sslStart).isGreaterThan(0L) - assertThat(timing.sslDuration).isGreaterThan(0L) - assertThat(timing.firstByteStart).isGreaterThan(0L) - assertThat(timing.firstByteDuration).isGreaterThan(0L) - assertThat(timing.downloadStart).isEqualTo(0L) - assertThat(timing.downloadDuration).isEqualTo(0L) - - assertThat(timing.connectStart) - .isGreaterThan(timing.dnsStart + timing.dnsDuration) - assertThat(timing.sslStart).isGreaterThan(timing.connectStart) - assertThat(timing.sslDuration).isLessThan(timing.connectDuration) - assertThat(timing.firstByteStart) - .isGreaterThan(timing.connectStart + timing.connectDuration) - } - } - - @Test - fun `𝕄 send timing info 𝕎 callEnd() for successful request`() { - // When - testedListener.callStart(mockCall) - Thread.sleep(10) - testedListener.dnsStart(mockCall, fakeDomain) - Thread.sleep(10) - testedListener.dnsEnd(mockCall, fakeDomain, emptyList()) - Thread.sleep(10) - testedListener.connectStart(mockCall, InetSocketAddress(0), Proxy.NO_PROXY) - Thread.sleep(10) - testedListener.secureConnectStart(mockCall) - Thread.sleep(10) - testedListener.secureConnectEnd(mockCall, null) - Thread.sleep(10) - testedListener.connectEnd(mockCall, InetSocketAddress(0), Proxy.NO_PROXY, Protocol.HTTP_2) - Thread.sleep(10) - testedListener.responseHeadersStart(mockCall) - Thread.sleep(10) - testedListener.responseHeadersEnd(mockCall, fakeResponse) - Thread.sleep(10) - testedListener.responseBodyStart(mockCall) - Thread.sleep(10) - testedListener.responseBodyEnd(mockCall, fakeByteCount) - Thread.sleep(10) - testedListener.callEnd(mockCall) - - // Then - argumentCaptor { - inOrder(mockMonitor, mockCall) { - verify(mockMonitor).waitForResourceTiming(fakeKey) - verify(mockMonitor).addResourceTiming(eq(fakeKey), capture()) - verifyNoMoreInteractions() - } - - val timing = firstValue - assertThat(timing.dnsStart).isGreaterThan(0L).isLessThan(TWENTY_MILLIS_NS) - assertThat(timing.dnsDuration).isGreaterThan(0L) - assertThat(timing.connectStart).isGreaterThan(0L) - assertThat(timing.connectDuration).isGreaterThan(0L) - assertThat(timing.sslStart).isGreaterThan(0L) - assertThat(timing.sslDuration).isGreaterThan(0L) - assertThat(timing.firstByteStart).isGreaterThan(0L) - assertThat(timing.firstByteDuration).isGreaterThan(0L) - assertThat(timing.downloadStart).isGreaterThan(0L) - assertThat(timing.downloadDuration).isGreaterThan(0L) - - assertThat(timing.connectStart) - .isGreaterThan(timing.dnsStart + timing.dnsDuration) - assertThat(timing.sslStart).isGreaterThan(timing.connectStart) - assertThat(timing.sslDuration).isLessThan(timing.connectDuration) - assertThat(timing.firstByteStart) - .isGreaterThan(timing.connectStart + timing.connectDuration) - assertThat(timing.downloadStart) - .isGreaterThan(timing.firstByteStart + timing.firstByteDuration) - } - } - - @Test - fun `𝕄 send timing info 𝕎 callEnd() for successful request with reused pool`() { - // When - testedListener.callStart(mockCall) - Thread.sleep(10) - testedListener.responseBodyStart(mockCall) - Thread.sleep(10) - testedListener.responseBodyEnd(mockCall, fakeByteCount) - Thread.sleep(10) - testedListener.callEnd(mockCall) - - // Then - argumentCaptor { - inOrder(mockMonitor, mockCall) { - verify(mockMonitor).waitForResourceTiming(fakeKey) - verify(mockMonitor).addResourceTiming(eq(fakeKey), capture()) - verifyNoMoreInteractions() - } - - val timing = firstValue - assertThat(timing.dnsStart).isEqualTo(0L) - assertThat(timing.dnsDuration).isEqualTo(0L) - assertThat(timing.connectStart).isEqualTo(0L) - assertThat(timing.connectDuration).isEqualTo(0L) - assertThat(timing.sslStart).isEqualTo(0L) - assertThat(timing.sslDuration).isEqualTo(0L) - assertThat(timing.firstByteStart).isEqualTo(0L) - assertThat(timing.firstByteDuration).isEqualTo(0L) - assertThat(timing.downloadStart).isGreaterThan(TimeUnit.NANOSECONDS.toMillis(10L)) - assertThat(timing.downloadDuration).isGreaterThan(TimeUnit.NANOSECONDS.toMillis(10L)) - } - } - - @Test - fun `𝕄 send timing info 𝕎 callFailed() for throwing request`( - @StringForgery error: String - ) { - // When - testedListener.callStart(mockCall) - Thread.sleep(10) - testedListener.dnsStart(mockCall, fakeDomain) - Thread.sleep(10) - testedListener.dnsEnd(mockCall, fakeDomain, emptyList()) - Thread.sleep(10) - testedListener.connectStart(mockCall, InetSocketAddress(0), Proxy.NO_PROXY) - Thread.sleep(10) - testedListener.secureConnectStart(mockCall) - Thread.sleep(10) - testedListener.secureConnectEnd(mockCall, null) - Thread.sleep(10) - testedListener.connectEnd(mockCall, InetSocketAddress(0), Proxy.NO_PROXY, Protocol.HTTP_2) - Thread.sleep(10) - testedListener.responseHeadersStart(mockCall) - Thread.sleep(10) - testedListener.responseHeadersEnd(mockCall, fakeResponse) - Thread.sleep(10) - testedListener.responseBodyStart(mockCall) - Thread.sleep(10) - testedListener.responseBodyEnd(mockCall, fakeByteCount) - Thread.sleep(10) - testedListener.callFailed(mockCall, IOException(error)) - - // Then - argumentCaptor { - inOrder(mockMonitor, mockCall) { - verify(mockMonitor).waitForResourceTiming(fakeKey) - verify(mockMonitor).addResourceTiming(eq(fakeKey), capture()) - verifyNoMoreInteractions() - } - - val timing = firstValue - assertThat(timing.dnsStart).isGreaterThan(0L).isLessThan(TWENTY_MILLIS_NS) - assertThat(timing.dnsDuration).isGreaterThan(0L) - assertThat(timing.connectStart).isGreaterThan(0L) - assertThat(timing.connectDuration).isGreaterThan(0L) - assertThat(timing.sslStart).isGreaterThan(0L) - assertThat(timing.sslDuration).isGreaterThan(0L) - assertThat(timing.firstByteStart).isGreaterThan(0L) - assertThat(timing.firstByteDuration).isGreaterThan(0L) - assertThat(timing.downloadStart).isGreaterThan(0L) - assertThat(timing.downloadDuration).isGreaterThan(0L) - - assertThat(timing.connectStart) - .isGreaterThan(timing.dnsStart + timing.dnsDuration) - assertThat(timing.sslStart).isGreaterThan(timing.connectStart) - assertThat(timing.sslDuration).isLessThan(timing.connectDuration) - assertThat(timing.firstByteStart) - .isGreaterThan(timing.connectStart + timing.connectDuration) - assertThat(timing.downloadStart) - .isGreaterThan(timing.firstByteStart + timing.firstByteDuration) - } - } - - @Test - fun `𝕄 doNothing 𝕎 call without RumMonitor`( - @IntForgery(400, 600) statusCode: Int - ) { - // Given - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.isRegistered.set(false) - - // When - testedListener.callStart(mockCall) - Thread.sleep(10) - testedListener.dnsStart(mockCall, fakeDomain) - Thread.sleep(10) - testedListener.dnsEnd(mockCall, fakeDomain, emptyList()) - Thread.sleep(10) - testedListener.connectStart(mockCall, InetSocketAddress(0), Proxy.NO_PROXY) - Thread.sleep(10) - testedListener.secureConnectStart(mockCall) - Thread.sleep(10) - testedListener.secureConnectEnd(mockCall, null) - Thread.sleep(10) - testedListener.connectEnd(mockCall, InetSocketAddress(0), Proxy.NO_PROXY, Protocol.HTTP_2) - Thread.sleep(10) - testedListener.responseHeadersStart(mockCall) - Thread.sleep(10) - testedListener.responseHeadersEnd(mockCall, fakeResponse) - - // Then - verifyZeroInteractions(mockMonitor) - } - - companion object { - val TWENTY_MILLIS_NS = TimeUnit.MILLISECONDS.toNanos(20) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogInterceptorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogInterceptorTest.kt deleted file mode 100644 index 25ae9fce13..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogInterceptorTest.kt +++ /dev/null @@ -1,162 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import com.datadog.android.core.internal.net.identifyRequest -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.tracing.TracingInterceptor -import com.datadog.android.tracing.TracingInterceptorTest -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Tracer -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogInterceptorTest : TracingInterceptorTest() { - - override fun instantiateTestedInterceptor( - tracedHosts: List, - factory: () -> Tracer - ): TracingInterceptor { - return DatadogInterceptor(tracedHosts, mockRequestListener, mockDetector, factory) - } - - @Test - fun `𝕄 start and stop RUM Resource 𝕎 intercept() for successful request`( - @IntForgery(min = 200, max = 300) statusCode: Int - ) { - // Given - stubChain(mockChain, statusCode) - val expectedStartAttrs = emptyMap() - val expectedStopAttrs = mapOf( - RumAttributes.TRACE_ID to fakeTraceId, - RumAttributes.SPAN_ID to fakeSpanId - ) - val requestId = identifyRequest(fakeRequest) - val mimeType = fakeMediaType?.type() - val kind = when { - fakeMethod in DatadogInterceptor.xhrMethods -> RumResourceKind.XHR - mimeType != null -> RumResourceKind.fromMimeType(mimeType) - else -> RumResourceKind.UNKNOWN - } - - // When - testedInterceptor.intercept(mockChain) - - // Then - inOrder(mockRumMonitor) { - verify(mockRumMonitor).startResource( - requestId, - fakeMethod, - fakeUrl, - expectedStartAttrs - ) - verify(mockRumMonitor).stopResource( - requestId, - statusCode, - fakeResponseBody.toByteArray().size.toLong(), - kind, - expectedStopAttrs - ) - } - } - - @Test - fun `𝕄 start and stop RUM Resource 𝕎 intercept() for failing request`( - @IntForgery(min = 400, max = 500) statusCode: Int - ) { - // Given - stubChain(mockChain, statusCode) - val expectedStartAttrs = emptyMap() - val expectedStopAttrs = mapOf( - RumAttributes.TRACE_ID to fakeTraceId, - RumAttributes.SPAN_ID to fakeSpanId - ) - val requestId = identifyRequest(fakeRequest) - val mimeType = fakeMediaType?.type() - val kind = when { - fakeMethod in DatadogInterceptor.xhrMethods -> RumResourceKind.XHR - mimeType != null -> RumResourceKind.fromMimeType(mimeType) - else -> RumResourceKind.UNKNOWN - } - - // When - testedInterceptor.intercept(mockChain) - - // Then - inOrder(mockRumMonitor) { - verify(mockRumMonitor).startResource( - requestId, - fakeMethod, - fakeUrl, - expectedStartAttrs - ) - verify(mockRumMonitor).stopResource( - requestId, - statusCode, - fakeResponseBody.toByteArray().size.toLong(), - kind, - expectedStopAttrs - ) - } - } - - @Test - fun `𝕄 start and stop RUM Resource 𝕎 intercept() for throwing request`( - @Forgery throwable: Throwable - ) { - // Given - val expectedStartAttrs = emptyMap() - val requestId = identifyRequest(fakeRequest) - whenever(mockChain.request()) doReturn fakeRequest - whenever(mockChain.proceed(any())) doThrow throwable - - // When - assertThrows(throwable.message.orEmpty()) { - testedInterceptor.intercept(mockChain) - } - - // Then - inOrder(mockRumMonitor) { - verify(mockRumMonitor).startResource( - requestId, - fakeMethod, - fakeUrl, - expectedStartAttrs - ) - verify(mockRumMonitor).stopResourceWithError( - requestId, - null, - "OkHttp request error $fakeMethod $fakeUrl", - RumErrorSource.NETWORK, - throwable - ) - } - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogInterceptorWithoutTracesTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogInterceptorWithoutTracesTest.kt deleted file mode 100644 index 9223720801..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogInterceptorWithoutTracesTest.kt +++ /dev/null @@ -1,311 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import android.content.Context -import android.util.Log -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.net.identifyRequest -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.tracing.TracedRequestListener -import com.datadog.android.tracing.TracingInterceptor -import com.datadog.android.tracing.TracingInterceptorTest -import com.datadog.android.tracing.internal.TracesFeature -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.android.utils.mockDevLogHandler -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Tracer -import okhttp3.Interceptor -import okhttp3.MediaType -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.Response -import okhttp3.ResponseBody -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogInterceptorWithoutTracesTest { - - lateinit var testedInterceptor: TracingInterceptor - - // region Mocks - - @Mock - lateinit var mockLocalTracer: Tracer - - @Mock - lateinit var mockChain: Interceptor.Chain - - @Mock - lateinit var mockRequestListener: TracedRequestListener - - lateinit var mockDevLogHandler: LogHandler - - lateinit var mockAppContext: Context - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Mock - lateinit var mockDetector: FirstPartyHostDetector - - // endregion - - // region Fakes - - @RegexForgery(TracingInterceptorTest.HOSTNAME_PATTERN) - lateinit var fakeHostName: String - - @RegexForgery(TracingInterceptorTest.IPV4_PATTERN) - lateinit var fakeHostIp: String - - lateinit var fakeMethod: String - var fakeBody: String? = null - var fakeMediaType: MediaType? = null - - @StringForgery(type = StringForgeryType.ASCII) - lateinit var fakeResponseBody: String - - lateinit var fakeUrl: String - - @Forgery - lateinit var fakeConfig: DatadogConfig.FeatureConfig - - @StringForgery - lateinit var fakePackageName: String - - @RegexForgery("\\d(\\.\\d){3}") - lateinit var fakePackageVersion: String - - lateinit var fakeRequest: Request - lateinit var fakeResponse: Response - - // endregion - - @BeforeEach - fun `set up`(forge: Forge) { - mockDevLogHandler = mockDevLogHandler() - mockAppContext = mockContext(fakePackageName, fakePackageVersion) - Datadog.setVerbosity(Log.VERBOSE) - - val mediaType = forge.anElementFrom("application", "image", "text", "model") + - "/" + forge.anAlphabeticalString() - fakeMediaType = MediaType.parse(mediaType) - fakeRequest = forgeRequest(forge) - testedInterceptor = DatadogInterceptor( - emptyList(), - mockRequestListener, - mockDetector - ) { mockLocalTracer } - TracesFeature.initialize( - mockAppContext, - fakeConfig, - mock(), mock(), mock(), mock(), mock(), mock(), mock(), TrackingConsentProvider() - ) - - GlobalRum.registerIfAbsent(mockRumMonitor) - } - - @AfterEach - fun `tear down`() { - GlobalRum.isRegistered.set(false) - TracesFeature.stop() - } - - @Test - fun `𝕄 start and stop RUM Resource 𝕎 intercept() for successful request`( - @IntForgery(min = 200, max = 300) statusCode: Int - ) { - // Given - stubChain(mockChain, statusCode) - val expectedStartAttrs = emptyMap() - val expectedStopAttrs = emptyMap() - val requestId = identifyRequest(fakeRequest) - val mimeType = fakeMediaType?.type() - val kind = when { - fakeMethod in DatadogInterceptor.xhrMethods -> RumResourceKind.XHR - mimeType != null -> RumResourceKind.fromMimeType(mimeType) - else -> RumResourceKind.UNKNOWN - } - - // When - testedInterceptor.intercept(mockChain) - - // Then - inOrder(mockRumMonitor) { - verify(mockRumMonitor).startResource( - requestId, - fakeMethod, - fakeUrl, - expectedStartAttrs - ) - verify(mockRumMonitor).stopResource( - requestId, - statusCode, - fakeResponseBody.toByteArray().size.toLong(), - kind, - expectedStopAttrs - ) - } - } - - @Test - fun `𝕄 start and stop RUM Resource 𝕎 intercept() for failing request`( - @IntForgery(min = 400, max = 500) statusCode: Int - ) { - // Given - stubChain(mockChain, statusCode) - val expectedStartAttrs = emptyMap() - val expectedStopAttrs = emptyMap() - val requestId = identifyRequest(fakeRequest) - val mimeType = fakeMediaType?.type() - val kind = when { - fakeMethod in DatadogInterceptor.xhrMethods -> RumResourceKind.XHR - mimeType != null -> RumResourceKind.fromMimeType(mimeType) - else -> RumResourceKind.UNKNOWN - } - - // When - testedInterceptor.intercept(mockChain) - - // Then - inOrder(mockRumMonitor) { - verify(mockRumMonitor).startResource( - requestId, - fakeMethod, - fakeUrl, - expectedStartAttrs - ) - verify(mockRumMonitor).stopResource( - requestId, - statusCode, - fakeResponseBody.toByteArray().size.toLong(), - kind, - expectedStopAttrs - ) - } - } - - @Test - fun `𝕄 starts and stop RUM Resource 𝕎 intercept() for throwing request`( - @Forgery throwable: Throwable - ) { - // Given - val expectedStartAttrs = emptyMap() - val requestId = identifyRequest(fakeRequest) - whenever(mockChain.request()) doReturn fakeRequest - whenever(mockChain.proceed(any())) doThrow throwable - - // When - assertThrows(throwable.message.orEmpty()) { - testedInterceptor.intercept(mockChain) - } - - // Then - inOrder(mockRumMonitor) { - verify(mockRumMonitor).startResource( - requestId, - fakeMethod, - fakeUrl, - expectedStartAttrs - ) - verify(mockRumMonitor).stopResourceWithError( - requestId, - null, - "OkHttp request error $fakeMethod $fakeUrl", - RumErrorSource.NETWORK, - throwable - ) - } - } - - // region Internal - - private fun stubChain(chain: Interceptor.Chain, statusCode: Int) { - fakeResponse = forgeResponse(statusCode) - - whenever(chain.request()) doReturn fakeRequest - whenever(chain.proceed(any())) doReturn fakeResponse - } - - private fun forgeRequest( - forge: Forge, - validHost: Boolean = true, - configure: (Request.Builder) -> Unit = {} - ): Request { - val protocol = forge.anElementFrom("http", "https") - val host = if (validHost) { - forge.anElementFrom(fakeHostIp, fakeHostName) - } else { - forge.aString(3) { anAlphabeticalChar() } + fakeHostName - } - - val path = forge.anAlphaNumericalString() - fakeUrl = "$protocol://$host/$path" - val builder = Request.Builder().url(http://github.com/fakeUrl) - if (forge.aBool()) { - fakeMethod = "POST" - fakeBody = forge.anAlphabeticalString() - builder.post(RequestBody.create(null, fakeBody!!.toByteArray())) - } else { - fakeMethod = forge.anElementFrom("GET", "HEAD", "DELETE") - fakeBody = null - builder.method(fakeMethod, null) - } - - configure(builder) - - return builder.build() - } - - private fun forgeResponse(statusCode: Int): Response { - val builder = Response.Builder() - .request(fakeRequest) - .protocol(Protocol.HTTP_2) - .code(statusCode) - .message("HTTP $statusCode") - .header(TracingInterceptor.HEADER_CT, fakeMediaType?.type().orEmpty()) - .body(ResponseBody.create(fakeMediaType, fakeResponseBody)) - return builder.build() - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogTest.kt deleted file mode 100644 index 87612a7508..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/DatadogTest.kt +++ /dev/null @@ -1,366 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android - -import android.app.Application -import android.content.Context -import android.content.pm.ApplicationInfo -import android.net.ConnectivityManager -import android.util.Log as AndroidLog -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.error.internal.CrashReportsFeature -import com.datadog.android.log.internal.LogsFeature -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.log.internal.user.MutableUserInfoProvider -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.tracing.internal.TracesFeature -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.android.utils.mockDevLogHandler -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.invokeMethod -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.BoolForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.UUID -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogTest { - - lateinit var mockAppContext: Application - - lateinit var mockDevLogHandler: LogHandler - - @Mock - lateinit var mockConnectivityMgr: ConnectivityManager - - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeToken: String - - @StringForgery - lateinit var fakePackageName: String - - @StringForgery(regex = "\\d(\\.\\d){3}") - lateinit var fakePackageVersion: String - - @StringForgery(regex = "[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]") - lateinit var fakeEnvName: String - - @TempDir - lateinit var tempRootDir: File - - lateinit var fakeConsent: TrackingConsent - - @BeforeEach - fun `set up`(forge: Forge) { - fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - mockDevLogHandler = mockDevLogHandler() - mockAppContext = mockContext(fakePackageName, fakePackageVersion) - whenever(mockAppContext.filesDir).thenReturn(tempRootDir) - whenever(mockAppContext.applicationContext) doReturn mockAppContext - whenever(mockAppContext.getSystemService(Context.CONNECTIVITY_SERVICE)) - .doReturn(mockConnectivityMgr) - } - - @AfterEach - fun `tear down`() { - Datadog.isDebug = false - try { - Datadog.invokeMethod("stop") - } catch (e: IllegalStateException) { - // nevermind - } - } - - @Test - fun `𝕄 do nothing 𝕎 stop() without initialize`() { - // When - Datadog.invokeMethod("stop") - - // Then - verifyZeroInteractions(mockAppContext) - } - - @Test - fun `𝕄 update userInfoProvider 𝕎 setUserInfo()`( - @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, - @StringForgery name: String, - @StringForgery(regex = "\\w+@\\w+") email: String - ) { - // Given - val mockUserInfoProvider = mock() - CoreFeature.userInfoProvider = mockUserInfoProvider - - // When - Datadog.setUserInfo(id, name, email) - - // Then - verify(mockUserInfoProvider).setUserInfo(UserInfo(id, name, email)) - } - - @Test - fun `𝕄 clears userInfoProvider 𝕎 setUserInfo() with defaults`( - @StringForgery(type = StringForgeryType.HEXADECIMAL) id: String, - @StringForgery name: String, - @StringForgery(regex = "\\w+@\\w+") email: String - ) { - // Given - val mockUserInfoProvider = mock() - CoreFeature.userInfoProvider = mockUserInfoProvider - - // When - Datadog.setUserInfo(id, name, email) - Datadog.setUserInfo() - - // Then - verify(mockUserInfoProvider).setUserInfo(UserInfo(id, name, email)) - verify(mockUserInfoProvider).setUserInfo(UserInfo(null, null, null)) - } - - @Test - fun `𝕄 return true 𝕎 initialize(context, consent, config) + isInitialized()`( - @Forgery applicationId: UUID - ) { - // Given - val config = DatadogConfig.Builder(fakeToken, fakeEnvName, applicationId) - .build() - - // When - Datadog.initialize(mockAppContext, fakeConsent, config) - val initialized = Datadog.isInitialized() - - // Then - assertThat(initialized).isTrue() - } - - @Test - fun `𝕄 initialize the ConsentProvider 𝕎 initializing)`( - @Forgery applicationId: UUID - ) { - // Given - val config = DatadogConfig.Builder(fakeToken, fakeEnvName, applicationId) - .build() - - // When - Datadog.initialize(mockAppContext, fakeConsent, config) - - // Then - assertThat(CoreFeature.trackingConsentProvider.getConsent()).isEqualTo(fakeConsent) - } - - @Test - fun `M update the ConsentProvider W setConsent`(forge: Forge) { - // GIVEN - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - val mockedConsentProvider: ConsentProvider = mock() - CoreFeature.trackingConsentProvider = mockedConsentProvider - - // WHEN - Datadog.setTrackingConsent(fakeConsent) - - // THEN - verify(CoreFeature.trackingConsentProvider).setConsent(fakeConsent) - } - - @Test - fun `M return false and log an error W initialize() {envName not valid, isDebug=false}`( - forge: Forge, - @Forgery applicationId: UUID - ) { - // Given - stubContextAsNotDebuggable(mockAppContext) - val fakeBadEnvName = forge.aStringMatching("^[\\$%\\*@][a-zA-Z0-9_:./-]{0,200}") - val config = DatadogConfig.Builder(fakeToken, fakeBadEnvName, applicationId) - .build() - - // When - Datadog.initialize(mockAppContext, fakeConsent, config) - val initialized = Datadog.isInitialized() - - // Then - verify(mockDevLogHandler).handleLog(AndroidLog.ERROR, Datadog.MESSAGE_ENV_NAME_NOT_VALID) - assertThat(initialized).isFalse() - } - - @Test - fun `M throw an exception W initialize() {envName not valid, isDebug=true}`( - forge: Forge, - @Forgery applicationId: UUID - ) { - // Given - stubContextAsDebuggable(mockAppContext) - val fakeBadEnvName = forge.aStringMatching("^[\\$%\\*@][a-zA-Z0-9_:./-]{0,200}") - val config = DatadogConfig.Builder(fakeToken, fakeBadEnvName, applicationId) - .build() - - // When - assertThatThrownBy { - Datadog.initialize( - mockAppContext, - fakeConsent, - config - ) - }.isInstanceOf(java.lang.IllegalArgumentException::class.java) - } - - @Test - fun `𝕄 return false 𝕎 isInitialized()`( - @Forgery applicationId: UUID - ) { - // When - val initialized = Datadog.isInitialized() - - // Then - assertThat(initialized).isFalse() - } - - @Test - fun `𝕄 initialize features 𝕎 initialize()`( - @Forgery applicationId: UUID - ) { - // Given - val config = DatadogConfig.Builder(fakeToken, fakeEnvName, applicationId) - .build() - - // When - Datadog.initialize(mockAppContext, fakeConsent, config) - - // Then - assertThat(CoreFeature.initialized.get()).isTrue() - assertThat(LogsFeature.initialized.get()).isTrue() - assertThat(CrashReportsFeature.initialized.get()).isTrue() - assertThat(TracesFeature.initialized.get()).isTrue() - assertThat(RumFeature.initialized.get()).isTrue() - } - - @Test - fun `𝕄 not initialize features 𝕎 initialize() with features disabled`( - @Forgery applicationId: UUID, - @BoolForgery logsEnabled: Boolean, - @BoolForgery crashReportEnabled: Boolean, - @BoolForgery tracesEnabled: Boolean, - @BoolForgery rumEnabled: Boolean - ) { - // Given - val config = DatadogConfig.Builder(fakeToken, fakeEnvName, applicationId) - .setLogsEnabled(logsEnabled) - .setCrashReportsEnabled(crashReportEnabled) - .setTracesEnabled(tracesEnabled) - .setRumEnabled(rumEnabled) - .build() - - // When - Datadog.initialize(mockAppContext, fakeConsent, config) - - // Then - assertThat(CoreFeature.initialized.get()).isTrue() - assertThat(LogsFeature.initialized.get()).isEqualTo(logsEnabled) - assertThat(CrashReportsFeature.initialized.get()).isEqualTo(crashReportEnabled) - assertThat(TracesFeature.initialized.get()).isEqualTo(tracesEnabled) - assertThat(RumFeature.initialized.get()).isEqualTo(rumEnabled) - } - - // region Deprecated - - @Test - fun `𝕄 return true 𝕎 initialize(context, config) + isInitialized()`( - @Forgery applicationId: UUID - ) { - // Given - val config = DatadogConfig.Builder(fakeToken, fakeEnvName, applicationId) - .build() - - // When - Datadog.initialize(mockAppContext, config) - val initialized = Datadog.isInitialized() - - // Then - assertThat(initialized).isTrue() - } - - @Test - fun `𝕄 bypass GDPR by default 𝕎 initialize(context, config) + isInitialized()`( - @Forgery applicationId: UUID - ) { - // Given - val config = DatadogConfig.Builder(fakeToken, fakeEnvName, applicationId) - .build() - - // When - Datadog.initialize(mockAppContext, config) - - // Then - assertThat(CoreFeature.trackingConsentProvider.getConsent()) - .isEqualTo(TrackingConsent.GRANTED) - } - - @Test - fun `𝕄 initialize features 𝕎 initialize(context, config) deprecated method`( - @Forgery applicationId: UUID - ) { - // Given - val config = DatadogConfig.Builder(fakeToken, fakeEnvName, applicationId) - .build() - - // When - Datadog.initialize(mockAppContext, config) - - // Then - assertThat(CoreFeature.initialized.get()).isTrue() - assertThat(LogsFeature.initialized.get()).isTrue() - assertThat(CrashReportsFeature.initialized.get()).isTrue() - assertThat(TracesFeature.initialized.get()).isTrue() - assertThat(RumFeature.initialized.get()).isTrue() - } - - // endregion - - // region Internal - - private fun stubContextAsDebuggable(mockContext: Context) { - val applicationInfo = mockContext.applicationInfo - applicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE - } - - private fun stubContextAsNotDebuggable(mockContext: Context) { - val applicationInfo = mockContext.applicationInfo - applicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE.inv() - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt deleted file mode 100644 index ab2586ce50..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt +++ /dev/null @@ -1,423 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal - -import android.app.ActivityManager -import android.app.Application -import android.content.BroadcastReceiver -import android.content.Context -import android.net.ConnectivityManager -import android.os.Build -import android.os.Process -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider -import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.privacy.NoOpConsentProvider -import com.datadog.android.core.internal.system.BroadcastReceiverSystemInfoProvider -import com.datadog.android.core.internal.time.NoOpTimeProvider -import com.datadog.android.log.internal.user.NoOpMutableUserInfoProvider -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.assertj.containsInstanceOf -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.atLeastOnce -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Locale -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.ThreadPoolExecutor -import okhttp3.ConnectionSpec -import okhttp3.Protocol -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class CoreFeatureTest { - - lateinit var mockAppContext: Application - - @Mock - lateinit var mockConnectivityMgr: ConnectivityManager - lateinit var fakePackageName: String - lateinit var fakePackageVersion: String - lateinit var fakeConsent: TrackingConsent - - @StringForgery(regex = "[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]") - lateinit var fakeEnvName: String - - @BeforeEach - fun `set up`(forge: Forge) { - fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - fakePackageName = forge.anAlphabeticalString() - fakePackageVersion = forge.aStringMatching("\\d(\\.\\d){3}") - - mockAppContext = mockContext(fakePackageName, fakePackageVersion) - whenever(mockAppContext.applicationContext) doReturn mockAppContext - whenever(mockAppContext.getSystemService(Context.CONNECTIVITY_SERVICE)) - .doReturn(mockConnectivityMgr) - } - - @AfterEach - fun `tear down`() { - CoreFeature.stop() - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `registers broadcast receivers on initialize (Lollipop)`() { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - val broadcastReceiverCaptor = argumentCaptor() - verify(mockAppContext, atLeastOnce()) - .registerReceiver(broadcastReceiverCaptor.capture(), any()) - - assertThat(broadcastReceiverCaptor.allValues) - .containsInstanceOf(BroadcastReceiverNetworkInfoProvider::class.java) - .containsInstanceOf(BroadcastReceiverSystemInfoProvider::class.java) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.N) - fun `registers receivers and callbacks on initialize (Nougat)`() { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - val broadcastReceiverCaptor = argumentCaptor() - verify(mockAppContext, atLeastOnce()) - .registerReceiver(broadcastReceiverCaptor.capture(), any()) - assertThat(broadcastReceiverCaptor.allValues) - .allMatch { it is BroadcastReceiverSystemInfoProvider } - verify(mockConnectivityMgr) - .registerDefaultNetworkCallback(isA()) - } - - @Test - fun `initializes time provider`() { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - assertThat(CoreFeature.timeProvider) - .isNotInstanceOf(NoOpTimeProvider::class.java) - } - - @Test - fun `initializes user info provider`() { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - assertThat(CoreFeature.userInfoProvider) - .isNotInstanceOf(NoOpMutableUserInfoProvider::class.java) - } - - @Test - fun `initializes app info`() { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - assertThat(CoreFeature.packageName).isEqualTo(fakePackageName) - assertThat(CoreFeature.packageVersion).isEqualTo(fakePackageVersion) - } - - @Test - fun `initializes first party hosts detector`( - @StringForgery(regex = "([a-zA-Z0-9]{3,9}\\.){1,4}[a-z]{3}") hosts: List - ) { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig(hosts = hosts)) - - val lowercaseHosts = hosts.map { it.toLowerCase(Locale.US) } - assertThat(CoreFeature.firstPartyHostDetector.knownHosts).containsAll(lowercaseHosts) - } - - @Test - fun `initializes all dependencies at initialize with null version name`( - @IntForgery(min = 0) versionCode: Int - ) { - mockAppContext = mockContext(fakePackageName, null, versionCode) - whenever(mockAppContext.applicationContext) doReturn mockAppContext - whenever(mockAppContext.getSystemService(Context.CONNECTIVITY_SERVICE)) - .doReturn(mockConnectivityMgr) - - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - assertThat(CoreFeature.packageName).isEqualTo(fakePackageName) - assertThat(CoreFeature.packageVersion).isEqualTo(versionCode.toString()) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `add strict network policy for https endpoints on 21+`(forge: Forge) { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - val okHttpClient = CoreFeature.okHttpClient - assertThat(okHttpClient.protocols()) - .containsExactly(Protocol.HTTP_2, Protocol.HTTP_1_1) - assertThat(okHttpClient.callTimeoutMillis()) - .isEqualTo(CoreFeature.NETWORK_TIMEOUT_MS.toInt()) - assertThat(okHttpClient.connectionSpecs()) - .containsExactly(ConnectionSpec.RESTRICTED_TLS) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.KITKAT) - fun `add compatibility network policy for https endpoints on 19+`(forge: Forge) { - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - val okHttpClient = CoreFeature.okHttpClient - assertThat(okHttpClient.protocols()) - .containsExactly(Protocol.HTTP_2, Protocol.HTTP_1_1) - assertThat(okHttpClient.callTimeoutMillis()) - .isEqualTo(CoreFeature.NETWORK_TIMEOUT_MS.toInt()) - assertThat(okHttpClient.connectionSpecs()) - .containsExactly(ConnectionSpec.MODERN_TLS) - } - - @Test - fun `no network policy for custom endpoints`(forge: Forge) { - CoreFeature.initialize( - mockAppContext, - fakeConsent, - DatadogConfig.CoreConfig(needsClearTextHttp = true) - ) - - val okHttpClient = CoreFeature.okHttpClient - assertThat(okHttpClient.protocols()) - .containsExactly(Protocol.HTTP_2, Protocol.HTTP_1_1) - assertThat(okHttpClient.callTimeoutMillis()) - .isEqualTo(CoreFeature.NETWORK_TIMEOUT_MS.toInt()) - assertThat(okHttpClient.connectionSpecs()) - .containsExactly(ConnectionSpec.CLEARTEXT) - } - - @Test - fun `stop will shutdown the executors`() { - // Given - CoreFeature.initialize( - mockAppContext, - fakeConsent, - DatadogConfig.CoreConfig(needsClearTextHttp = true) - ) - val mockThreadPoolExecutor: ThreadPoolExecutor = mock() - CoreFeature.dataPersistenceExecutorService = mockThreadPoolExecutor - val mockScheduledThreadPoolExecutor: ScheduledThreadPoolExecutor = mock() - CoreFeature.dataUploadScheduledExecutor = mockScheduledThreadPoolExecutor - - // When - CoreFeature.stop() - - // Then - verify(mockThreadPoolExecutor).shutdownNow() - verify(mockScheduledThreadPoolExecutor).shutdownNow() - } - - @Test - fun `if custom service name not provided will use the package name`() { - // Given - CoreFeature.initialize( - mockAppContext, - fakeConsent, - DatadogConfig.CoreConfig(serviceName = null) - ) - - // Then - assertThat(CoreFeature.serviceName).isEqualTo(mockAppContext.packageName) - } - - @Test - fun `if custom service name provided will use this instead of the package name`(forge: Forge) { - // Given - val serviceName = forge.anAlphabeticalString() - CoreFeature.initialize( - mockAppContext, - fakeConsent, - DatadogConfig.CoreConfig(serviceName = serviceName) - ) - - // Then - assertThat(CoreFeature.serviceName).isEqualTo(serviceName) - } - - @Test - fun `if this process name matches the package name it will be marked as main process`( - forge: Forge - ) { - // Given - val mockActivityManager = mock() - whenever(mockAppContext.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( - mockActivityManager - ) - val myProcess = forgeAppProcessInfo( - Process.myPid(), - fakePackageName - ) - val otherProcess = forgeAppProcessInfo( - Process.myPid() + 1, - forge.anAlphabeticalString() - ) - otherProcess.processName = forge.anAlphabeticalString() - otherProcess.pid = Process.myPid() + 1 - whenever(mockActivityManager.runningAppProcesses) - .thenReturn( - listOf(myProcess, otherProcess) - ) - - // When - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - // Then - assertThat(CoreFeature.isMainProcess).isTrue() - } - - @Test - fun `if this process does not match the package name it will be marked as secondary process`( - forge: Forge - ) { - // Given - val mockActivityManager = mock() - whenever(mockAppContext.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( - mockActivityManager - ) - val myProcess = forgeAppProcessInfo( - Process.myPid(), - fakePackageName + forge.anAlphabeticalString(size = 1) - ) - val otherProcess = forgeAppProcessInfo( - Process.myPid() + 1, - forge.anAlphabeticalString() - ) - otherProcess.processName = forge.anAlphabeticalString() - otherProcess.pid = Process.myPid() + 1 - whenever(mockActivityManager.runningAppProcesses) - .thenReturn( - listOf(myProcess, otherProcess) - ) - - // When - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - // Then - assertThat(CoreFeature.isMainProcess).isFalse() - } - - @Test - fun `will mark it as main process by default if could not be found in the list`(forge: Forge) { - // Given - val mockActivityManager = mock() - whenever(mockAppContext.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn( - mockActivityManager - ) - val otherProcess = forgeAppProcessInfo( - Process.myPid() + 1, - forge.anAlphabeticalString() - ) - otherProcess.processName = forge.anAlphabeticalString() - otherProcess.pid = Process.myPid() + 1 - whenever(mockActivityManager.runningAppProcesses) - .thenReturn( - listOf(otherProcess) - ) - - // When - CoreFeature.initialize(mockAppContext, fakeConsent, DatadogConfig.CoreConfig()) - - // Then - assertThat(CoreFeature.isMainProcess).isTrue() - } - - @Test - fun `M initialise the env name W provided from Config`() { - // WHEN - CoreFeature.initialize( - mockAppContext, - fakeConsent, - DatadogConfig.CoreConfig(envName = fakeEnvName) - ) - - // THEN - assertThat(CoreFeature.envName).isEqualTo(fakeEnvName) - } - - @Test - fun `M initialise the ConsentProvider`() { - // WHEN - CoreFeature.initialize( - mockAppContext, - fakeConsent, - DatadogConfig.CoreConfig(envName = fakeEnvName) - ) - - // THEN - assertThat(CoreFeature.trackingConsentProvider.getConsent()).isEqualTo(fakeConsent) - } - - @Test - fun `M use a NoOpConsentProvider by default`() { - assertThat(CoreFeature.trackingConsentProvider) - .isInstanceOf(NoOpConsentProvider::class.java) - } - - @Test - fun `M unregister an use a NoOpConsentProvider W stopped`() { - // GIVEN - val mockedConsentProvider: ConsentProvider = mock() - CoreFeature.initialize( - mockAppContext, - fakeConsent, - DatadogConfig.CoreConfig(envName = fakeEnvName) - ) - CoreFeature.trackingConsentProvider = mockedConsentProvider - - // WHEN - CoreFeature.stop() - - // THEN - verify(mockedConsentProvider).unregisterAllCallbacks() - assertThat(CoreFeature.trackingConsentProvider).isInstanceOf( - NoOpConsentProvider::class.java - ) - } - - // region internal - - private fun forgeAppProcessInfo( - processId: Int, - processName: String - ): ActivityManager.RunningAppProcessInfo { - return ActivityManager.RunningAppProcessInfo( - "", - 0, - emptyArray() - ).apply { - this.processName = processName - this.pid = processId - } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/constraints/DatadogDataConstraintsTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/constraints/DatadogDataConstraintsTest.kt deleted file mode 100644 index edcd6be008..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/constraints/DatadogDataConstraintsTest.kt +++ /dev/null @@ -1,294 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.constraints - -import android.os.Build -import android.util.Log -import com.datadog.android.Datadog -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockDevLogHandler -import com.datadog.android.utils.times -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.Case -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Locale -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings() -@ForgeConfiguration(Configurator::class) -internal class DatadogDataConstraintsTest { - - lateinit var testedConstraints: DataConstraints - - lateinit var mockDevLogHandler: LogHandler - - @BeforeEach - fun `set up`() { - Datadog.setVerbosity(Log.VERBOSE) - // we need to set the Build.MODEL to null, to override the setup - Build::class.java.setStaticValue("MODEL", null) - - mockDevLogHandler = mockDevLogHandler() - - testedConstraints = DatadogDataConstraints() - } - - // region Tags - - @Test - fun `keep valid tag`(forge: Forge) { - val tag = forge.aStringMatching("[a-z]([a-z0-9_:./-]{0,198}[a-z0-9_./-])?") - - val result = testedConstraints.validateTags(listOf(tag)) - - assertThat(result).containsOnly(tag) - verifyZeroInteractions(mockDevLogHandler) - } - - @Test - fun `ignore invalid tag - start with a letter`(forge: Forge) { - val key = forge.aStringMatching("\\d[a-z]+") - val value = forge.aNumericalString() - val tag = "$key:$value" - - val result = testedConstraints.validateTags(listOf(tag)) - - assertThat(result).isEmpty() - verify(mockDevLogHandler) - .handleLog(Log.ERROR, "\"$tag\" is an invalid tag, and was ignored.") - } - - @Test - fun `replace illegal characters`(forge: Forge) { - val validPart = forge.anAlphabeticalString(size = 3) - val invalidPart = forge.aString { - anElementFrom(',', '?', '%', '(', ')', '[', ']', '{', '}') - } - val value = forge.aNumericalString() - val tag = "$validPart$invalidPart:$value" - - val result = testedConstraints.validateTags(listOf(tag)) - - val converted = '_' * invalidPart.length - val expectedCorrectedTag = "$validPart$converted:$value" - assertThat(result) - .containsOnly(expectedCorrectedTag) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "tag \"$tag\" was modified to \"$expectedCorrectedTag\" to match our constraints." - ) - } - - @Test - fun `convert uppercase key to lowercase`(forge: Forge) { - val key = forge.anAlphabeticalString(case = Case.UPPER) - val value = forge.aNumericalString() - val tag = "$key:$value" - - val result = testedConstraints.validateTags(listOf(tag)) - - val expectedCorrectedTag = "${key.toLowerCase(Locale.US)}:$value" - assertThat(result) - .containsOnly(expectedCorrectedTag) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "tag \"$tag\" was modified to \"$expectedCorrectedTag\" to match our constraints." - ) - } - - @Test - fun `trim tags over 200 characters`(forge: Forge) { - val tag = forge.anAlphabeticalString(size = forge.aSmallInt() + 200) - - val result = testedConstraints.validateTags(listOf(tag)) - - val expectedCorrectedTag = tag.substring(0, 200) - assertThat(result) - .containsOnly(expectedCorrectedTag) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "tag \"$tag\" was modified to \"$expectedCorrectedTag\" to match our constraints." - ) - } - - @Test - fun `trim tags ending with a colon`(forge: Forge) { - val expectedCorrectedTag = forge.anAlphabeticalString() - - val result = testedConstraints.validateTags(listOf("$expectedCorrectedTag:")) - - assertThat(result) - .containsOnly(expectedCorrectedTag) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "tag \"$expectedCorrectedTag:\" was modified to " + - "\"$expectedCorrectedTag\" to match our constraints." - ) - } - - @Test - fun `ignore reserved tag keys`(forge: Forge) { - val key = forge.anElementFrom("host", "device", "source", "service") - val value = forge.aNumericalString() - val invalidTag = "$key:$value" - - val result = testedConstraints.validateTags(listOf(invalidTag)) - - assertThat(result) - .isEmpty() - verify(mockDevLogHandler).handleLog( - Log.ERROR, - "\"$invalidTag\" is an invalid tag, and was ignored." - ) - } - - @Test - fun `ignore reserved tag keys (workaround)`(forge: Forge) { - val key = forge.randomizeCase { anElementFrom("host", "device", "source", "service") } - val value = forge.aNumericalString() - val invalidTag = "$key:$value" - - val result = testedConstraints.validateTags(listOf(invalidTag)) - - assertThat(result) - .isEmpty() - verify(mockDevLogHandler).handleLog( - Log.ERROR, - "\"$invalidTag\" is an invalid tag, and was ignored." - ) - } - - @Test - fun `ignore tag if adding more than 100`(forge: Forge) { - val tags = forge.aList(128) { aStringMatching("[a-z]{1,8}:[0-9]{1,8}") } - val firstTags = tags.take(100) - - val result = testedConstraints.validateTags(tags) - - val discardedCount = tags.size - 100 - assertThat(result) - .containsExactlyElementsOf(firstTags) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "too many tags were added, $discardedCount had to be discarded." - ) - } - - //endregion - - // region Attributes - - @Test - fun `keep valid attribute`( - forge: Forge - ) { - val key = forge.anAlphabeticalString() - val value = forge.aNumericalString() - - val result = testedConstraints.validateAttributes(mapOf(key to value)) - - assertThat(result) - .containsEntry(key, value) - verifyZeroInteractions(mockDevLogHandler) - } - - @Test - fun `M convert nested attribute keys W over 10 levels`(forge: Forge) { - val topLevels = forge.aList(10) { anAlphabeticalString() } - val lowerLevels = forge.aList { anAlphabeticalString() } - val key = (topLevels + lowerLevels).joinToString(".") - val value = forge.aNumericalString() - - val result = testedConstraints.validateAttributes(mapOf(key to value)) - - val expectedKey = topLevels.joinToString(".") + "_" + lowerLevels.joinToString("_") - assertThat(result) - .containsEntry(expectedKey, value) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "Key \"$key\" was modified to \"$expectedKey\" to match our constraints." - ) - } - - @Test - fun `M convert nested attribute keys W over 10 levels and using prefix`(forge: Forge) { - val keyPrefix = forge - .aList(5) { forge.anAlphabeticalString() } - .joinToString(".") - val topLevels = forge.aList(5) { anAlphabeticalString() } - val lowerLevels = forge.aList { anAlphabeticalString() } - val key = (topLevels + lowerLevels).joinToString(".") - val value = forge.aNumericalString() - val result = - testedConstraints.validateAttributes(mapOf(key to value), keyPrefix = keyPrefix) - - val expectedKey = topLevels.joinToString(".") + "_" + lowerLevels.joinToString("_") - assertThat(result) - .containsEntry(expectedKey, value) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "Key \"$key\" was modified to \"$expectedKey\" to match our constraints." - ) - } - - @Test - fun `ignore attribute if adding more than 128`(forge: Forge) { - val attributes = forge.aList(202) { anAlphabeticalString() to anInt() }.toMap() - val firstAttributes = attributes.toList().take(128).toMap() - - val result = testedConstraints.validateAttributes(attributes) - - val discardedCount = attributes.size - 128 - assertThat(result) - .hasSize(128) - .containsAllEntriesOf(firstAttributes) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "Too many attributes were added, $discardedCount had to be discarded." - ) - } - - @Test - fun `M use a custom error format W validateAttributes`(forge: Forge) { - val attributes = forge.aList(202) { anAlphabeticalString() to anInt() }.toMap() - val firstAttributes = attributes.toList().take(128).toMap() - val fakeAttributesGroup = forge - .aList(size = 10) { forge.anAlphabeticalString() } - .joinToString(".") - val result = testedConstraints.validateAttributes( - attributes, - attributesGroupName = fakeAttributesGroup - ) - - val discardedCount = attributes.size - 128 - assertThat(result) - .hasSize(128) - .containsAllEntriesOf(firstAttributes) - verify(mockDevLogHandler).handleLog( - Log.WARN, - "Too many attributes were added for [$fakeAttributesGroup]," + - " $discardedCount had to be discarded." - ) - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/DeferredWriterTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/DeferredWriterTest.kt deleted file mode 100644 index 23de6ff4e8..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/DeferredWriterTest.kt +++ /dev/null @@ -1,202 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import android.os.Build -import com.datadog.android.core.internal.data.DataMigrator -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.getFieldValue -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.reset -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ExecutorService -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class DeferredWriterTest { - - lateinit var testedWriter: DeferredWriter - - @Mock - lateinit var mockDelegate: Writer - - @Mock - lateinit var mockDataMigrator: DataMigrator - - @Mock - lateinit var mockExecutorService: ExecutorService - - lateinit var threadName: String - - @BeforeEach - fun `set up`(forge: Forge) { - threadName = forge.anAlphabeticalString() - whenever(mockExecutorService.submit(any())).doAnswer { - (it.arguments[0] as Runnable).run() - mock() - } - testedWriter = DeferredWriter( - mockDelegate, - mockExecutorService, - mockDataMigrator - ) - } - - @Test - fun `migrates the data before doing anything else`(forge: Forge) { - val model = forge.anAlphabeticalString() - - // When - testedWriter.write(model) - - // Then - val inOrder = inOrder(mockDataMigrator, mockDelegate) - inOrder.verify(mockDataMigrator).migrateData() - inOrder.verify(mockDelegate).write(model) - } - - @Test - fun `migrates the data before doing anything else in multi thread`(forge: Forge) { - val model1 = forge.anAlphabeticalString() - val model2 = forge.anAlphabeticalString() - val model3 = forge.anAlphabeticalString() - - val countDownLatch = CountDownLatch(3) - - // When - Thread { - testedWriter.write(model1) - countDownLatch.countDown() - }.start() - Thread { - testedWriter.write(model2) - countDownLatch.countDown() - }.start() - Thread { - testedWriter.write(model3) - countDownLatch.countDown() - }.start() - - countDownLatch.await(3000, TimeUnit.SECONDS) - - // Then - inOrder(mockDataMigrator, mockDelegate) { - verify(mockDataMigrator).migrateData() - argumentCaptor() { - verify(mockDelegate, times(3)).write(capture()) - assertThat(allValues).containsExactlyInAnyOrder(model1, model2, model3) - } - } - } - - @Test - fun `handles the data correctly even when write was called before migration`(forge: Forge) { - val model1 = forge.anAlphabeticalString() - val model2 = forge.anAlphabeticalString() - val model3 = forge.anAlphabeticalString() - - val countDownLatch = CountDownLatch(3) - val dataMigrated: AtomicBoolean = testedWriter.getFieldValue("dataMigrated") - dataMigrated.set(false) - - // When - Thread { - testedWriter.write(model1) - countDownLatch.countDown() - }.start() - - Thread { - testedWriter.write(model2) - countDownLatch.countDown() - }.start() - - // simulate data migration finalized - dataMigrated.set(true) - - Thread { - testedWriter.write(model3) - countDownLatch.countDown() - }.start() - - countDownLatch.await(3000, TimeUnit.SECONDS) - - // Then - inOrder(mockDataMigrator, mockDelegate) { - verify(mockDataMigrator).migrateData() - argumentCaptor() { - verify(mockDelegate, times(3)).write(capture()) - assertThat(allValues).containsExactlyInAnyOrder(model1, model2, model3) - } - } - } - - @Test - fun `if no data migrator provided will not perform the migration step`(forge: Forge) { - val model = forge.anAlphabeticalString() - reset(mockExecutorService) - testedWriter = DeferredWriter( - mockDelegate, - mockExecutorService - ) - // When - testedWriter.write(model) - - // Then - verify(mockExecutorService, times(1)).submit(any()) - verifyNoMoreInteractions(mockExecutorService) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `run delegate in deferred handler when writing a model`(forge: Forge) { - val model = forge.anAlphabeticalString() - - testedWriter.write(model) - - verify(mockDelegate).write(model) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `run delegate in deferred handler when writing a models list`(forge: Forge) { - val models: List = forge.aList(size = 10) { forge.anAlphabeticalString() } - - testedWriter.write(models) - - verify(mockDelegate).write(models) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileExtensionsTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileExtensionsTest.kt deleted file mode 100644 index b2be17861d..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileExtensionsTest.kt +++ /dev/null @@ -1,76 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.spy -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings() -class FileExtensionsTest { - - @TempDir - lateinit var tempDir: File - - lateinit var fakePrefix: String - lateinit var fakeSuffix: String - - @BeforeEach - fun `set up`(forge: Forge) { - fakePrefix = forge.anAsciiString(size = forge.anInt(min = 2, max = 8)) - fakeSuffix = forge.anAsciiString(size = forge.anInt(min = 2, max = 8)) - } - - @Test - fun `adds the suffix and prefix to the file ByteArray`(forge: Forge) { - val file = File(tempDir, "testFile") - file.createNewFile() - val dataToWrite = forge.anAlphaNumericalString() - file.writeText(dataToWrite) - - val readData = file.readBytes(fakePrefix, fakeSuffix) - assertThat(String(readData)).isEqualTo("$fakePrefix$dataToWrite$fakeSuffix") - } - - @Test - fun `adds the suffix and prefix to an empty file`() { - val file = File(tempDir, "testFile") - file.createNewFile() - - val readData = file.readBytes(fakePrefix, fakeSuffix) - assertThat(String(readData)).isEqualTo("$fakePrefix$fakeSuffix") - } - - @Test - fun `returns empty ByteArray if the file is too big`(forge: Forge) { - val file = File(tempDir, "testFile") - file.createNewFile() - val spiedFile = spy(file) - doReturn(Long.MAX_VALUE).whenever(spiedFile).length() - - val readData = spiedFile.readBytes(fakePrefix, fakeSuffix) - assertThat(readData).isEmpty() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileFilterTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileFilterTest.kt deleted file mode 100644 index 25bb22d219..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileFilterTest.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.io.FileFilter -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings() -internal class FileFilterTest { - - @TempDir - lateinit var tempDir: File - - lateinit var testedFilter: FileFilter - - @BeforeEach - fun `set up`() { - testedFilter = FileFilter() - } - - @Test - fun `does not accept null files`() { - val file: File? = null - - val accepted = testedFilter.accept(file) - - assertThat(accepted) - .isFalse() - } - - @Test - fun `does not accept directory`(forge: Forge) { - val fileName = forge.aNumericalString() - val dir = File(tempDir, fileName) - dir.mkdirs() - - val accepted = testedFilter.accept(dir) - - assertThat(accepted) - .isFalse() - } - - @Test - fun `does not accept file with at least one invalid char`(forge: Forge) { - val fileName = forge.aNumericalString() + - forge.anAlphabeticalChar() + - forge.aNumericalString() - val file = File(tempDir, fileName) - file.writeText(forge.anAlphabeticalString()) - - val accepted = testedFilter.accept(file) - - assertThat(accepted) - .isFalse() - } - - @Test - fun `accepts a file with digit only name`(forge: Forge) { - val fileName = forge.aNumericalString() - val file = File(tempDir, fileName) - file.writeText(forge.anAlphabeticalString()) - - val accepted = testedFilter.accept(file) - - assertThat(accepted) - .isTrue() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileHandlerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileHandlerTest.kt deleted file mode 100644 index e2d4d02886..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileHandlerTest.kt +++ /dev/null @@ -1,354 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import android.util.Log -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.utils.mockSdkLogHandler -import com.datadog.android.utils.restoreSdkLogHandler -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.lang.NullPointerException -import java.lang.RuntimeException -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class FileHandlerTest { - - @TempDir - lateinit var fakeRootDirectory: File - - lateinit var fakeSourceDirectory: File - lateinit var fakeDestinationDirectory: File - - lateinit var testedFileHandler: FileHandler - - @StringForgery(regex = "([a-z]+)-([a-z]+)") - lateinit var fakeSourceDirName: String - - @StringForgery(regex = "([a-z]+)-([a-z]+)") - lateinit var fakeDestinationDirName: String - - @BeforeEach - fun `set up`() { - testedFileHandler = FileHandler() - fakeSourceDirectory = File(fakeRootDirectory, fakeSourceDirName) - fakeDestinationDirectory = File(fakeRootDirectory, fakeDestinationDirName) - } - - // region MoveFiles - - @Test - fun `M return true W moveFiles { source directory does not exist}`( - forge: Forge - ) { - // WHEN - val success = - testedFileHandler.moveFiles( - fakeSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - assertThat(success).isTrue() - } - - @Test - fun `M send a warning log W moveFiles { source directory does not exist}`( - forge: Forge - ) { - // GIVEN - val mockLogHandler: LogHandler = mock() - val originalLogHandler: LogHandler = mockSdkLogHandler(mockLogHandler) - - // WHEN - testedFileHandler.moveFiles( - fakeSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - verify(mockLogHandler).handleLog( - Log.WARN, - "There were no files to move. " + - "There is no directory at this path: [${fakeSourceDirectory.absolutePath}]" - ) - restoreSdkLogHandler(originalLogHandler) - } - - @Test - fun `M return true W moveFiles { source directory is not a directory}`( - forge: Forge - ) { - // GIVEN - fakeSourceDirectory.createNewFile() - - // WHEN - val success = - testedFileHandler.moveFiles( - fakeSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - assertThat(success).isTrue() - } - - @Test - fun `M send a warning log W moveFiles { source directory is not a directory}`( - forge: Forge - ) { - // GIVEN - val mockLogHandler: LogHandler = mock() - val originalLogHandler: LogHandler = mockSdkLogHandler(mockLogHandler) - fakeSourceDirectory.createNewFile() - - // WHEN - testedFileHandler.moveFiles( - fakeSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - verify(mockLogHandler).handleLog( - Log.WARN, - "There were no files to move." + - "[${fakeSourceDirectory.absolutePath}] is not a directory." - ) - restoreSdkLogHandler(originalLogHandler) - } - - @Test - fun `M move all the files W moveFiles`(forge: Forge) { - // GIVEN - fakeSourceDirectory.mkdirs() - fakeDestinationDirectory.mkdirs() - val files = forge.aList { - File(fakeSourceDirectory, forge.anAlphabeticalString()) - } - files.forEach { it.createNewFile() } - - // WHEN - val success = testedFileHandler.moveFiles( - fakeSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - assertThat(success).isTrue() - assertThat(fakeSourceDirectory.listFiles()).isEmpty() - val destinationDirectoryFiles = fakeDestinationDirectory.listFiles() - assertThat(destinationDirectoryFiles?.map { it.name }) - .containsOnly(*(files.map { it.name }.toTypedArray())) - } - - @Test - fun `M return true W moveFiles { sourceDirectory is empty }`(forge: Forge) { - // GIVEN - fakeSourceDirectory.mkdirs() - fakeDestinationDirectory.mkdirs() - - // WHEN - val success = testedFileHandler.moveFiles( - fakeSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - assertThat(success).isTrue() - val destinationDirectoryFiles = fakeDestinationDirectory.listFiles() - assertThat(destinationDirectoryFiles).isEmpty() - } - - @Test - fun `M create the destination directory if does not exists W moveFiles`(forge: Forge) { - // GIVEN - fakeSourceDirectory.mkdirs() - - // WHEN - val success = testedFileHandler.moveFiles( - fakeSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - assertThat(success).isTrue() - val destinationDirectoryFiles = fakeDestinationDirectory.listFiles() - assertThat(destinationDirectoryFiles).isEmpty() - } - - @Test - fun `M return false W moveFiles { renameFile fails with NPE }`(forge: Forge) { - // GIVEN - val fakeNpe = NullPointerException(forge.anAlphabeticalString()) - val mockedFilesPair = mockedFilesWithSpecialMock(forge) { - whenever(it.renameTo(any())).doThrow(fakeNpe) - } - val mockLogHandler: LogHandler = mock() - val originalLogHandler: LogHandler = mockSdkLogHandler(mockLogHandler) - val brokenFile = mockedFilesPair.second - val mockedSourceDirectory: File = mock { - whenever(it.isDirectory).thenReturn(true) - whenever(it.exists()).thenReturn(true) - whenever(it.listFiles()).thenReturn(mockedFilesPair.first) - } - - // WHEN - val success = testedFileHandler.moveFiles( - mockedSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - assertThat(success).isFalse() - verify(mockLogHandler).handleLog( - Log.ERROR, - "Unable to move file: [${brokenFile.absolutePath}]" + - " to new file: " + - "[${fakeDestinationDirectory.absolutePath}/${brokenFile.name}]", - fakeNpe - ) - restoreSdkLogHandler(originalLogHandler) - } - - @Test - fun `M return false W moveFiles { renameFile fails with SecurityException }`(forge: Forge) { - // GIVEN - val fakeSecException = SecurityException(forge.anAlphabeticalString()) - val mockedFilesPair = mockedFilesWithSpecialMock(forge) { - whenever(it.renameTo(any())).doThrow(fakeSecException) - } - val mockLogHandler: LogHandler = mock() - val originalLogHandler: LogHandler = mockSdkLogHandler(mockLogHandler) - val brokenFile = mockedFilesPair.second - val mockedSourceDirectory: File = mock { - whenever(it.isDirectory).thenReturn(true) - whenever(it.exists()).thenReturn(true) - whenever(it.listFiles()).thenReturn(mockedFilesPair.first) - } - - // WHEN - val success = testedFileHandler.moveFiles( - mockedSourceDirectory, - fakeDestinationDirectory - ) - - // THEN - assertThat(success).isFalse() - verify(mockLogHandler).handleLog( - Log.ERROR, - "Unable to move file: [${brokenFile.absolutePath}]" + - " to new file: " + - "[${fakeDestinationDirectory.absolutePath}/${brokenFile.name}]", - fakeSecException - ) - restoreSdkLogHandler(originalLogHandler) - } - - // endregion - - // region CleanDirectory - - @Test - fun `M clear the file W required`() { - // GIVEN - val mockDirectory: File = mock { - whenever(it.deleteRecursively()).thenReturn(true) - } - - // WHEN - val success = testedFileHandler.deleteFileOrDirectory(mockDirectory) - - // THEN - assertThat(success).isTrue() - } - - @Test - fun `M return false W clearFile { throws Exception }`(forge: Forge) { - // GIVEN - val fakeException = RuntimeException(forge.anAlphabeticalString()) - val mockFile: File = mock { - whenever(it.deleteRecursively()).thenThrow(fakeException) - } - - // WHEN - val success = testedFileHandler.deleteFileOrDirectory(mockFile) - - // THEN - assertThat(success).isFalse() - } - - @Test - fun `M send an error log W clearFile { throws Exception }`(forge: Forge) { - // GIVEN - val mockLogHandler: LogHandler = mock() - val originalLogHandler: LogHandler = mockSdkLogHandler(mockLogHandler) - val fakeException = RuntimeException(forge.anAlphabeticalString()) - val mockFile: File = mock { - whenever(it.deleteRecursively()).thenThrow(fakeException) - whenever(it.absolutePath).thenReturn(forge.anAlphabeticalString()) - } - - // WHEN - testedFileHandler.deleteFileOrDirectory(mockFile) - - // THEN - verify(mockLogHandler).handleLog( - Log.ERROR, - "Unable to clear the file at [${mockFile.absolutePath}]", - fakeException - ) - restoreSdkLogHandler(originalLogHandler) - } - - // endregion - - // region Internal - - private fun mockedFilesWithSpecialMock( - forge: Forge, - specialMockBlock: (File) -> Unit - ): Pair, File> { - val filesLength = forge.anInt(1, 10) - val specialMockIndex = forge.anInt(0, filesLength) - val mockedFiles = Array(filesLength) { index -> - mock { - if (index == specialMockIndex) { - specialMockBlock(it) - } else { - whenever(it.renameTo(any())).thenReturn(true) - } - val fakeFileName = forge.anAlphabeticalString() - whenever(it.name).thenReturn(fakeFileName) - whenever(it.absolutePath) - .thenReturn(forge.aStringMatching("([a-z]+)/([a-z]+)/") + fakeFileName) - } - } - return mockedFiles to mockedFiles[specialMockIndex] - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileReaderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileReaderTest.kt deleted file mode 100644 index 009af13ee9..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/FileReaderTest.kt +++ /dev/null @@ -1,394 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import android.os.Build -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.getFieldValue -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class FileReaderTest { - - lateinit var testedReader: FileReader - - @TempDir - lateinit var tempRootDir: File - - @Mock - lateinit var mockOrchestrator: Orchestrator - - lateinit var fakePrefix: String - lateinit var fakeSuffix: String - - @BeforeEach - fun `set up`(forge: Forge) { - fakePrefix = forge.anAsciiString(size = forge.anInt(min = 2, max = 8)) - fakeSuffix = forge.anAsciiString(size = forge.anInt(min = 2, max = 8)) - testedReader = FileReader(mockOrchestrator, tempRootDir, fakePrefix, fakeSuffix) - } - - @Test - fun `doesn't ask for the same batch twice in a row`( - forge: Forge - ) { - val fileName = forge.anAlphabeticalString() - val file = forgeTempFile(fileName) - val data = forge.anAlphabeticalString() - file.writeText(data) - whenever(mockOrchestrator.getReadableFile(any())) doReturn null - whenever(mockOrchestrator.getReadableFile(emptySet())) doReturn file - - val firstBatch = testedReader.readNextBatch() - val secondBatch = testedReader.readNextBatch() - checkNotNull(firstBatch) - - assertThat(String(firstBatch.data)).isEqualTo("$fakePrefix$data$fakeSuffix") - assertThat(secondBatch).isNull() - inOrder(mockOrchestrator) { - verify(mockOrchestrator).getReadableFile(emptySet()) - verify(mockOrchestrator).getReadableFile(setOf(firstBatch.id)) - verifyNoMoreInteractions(mockOrchestrator) - } - } - - @Test - fun `reads a batch that was previously read then released`( - forge: Forge - ) { - val fileName = forge.anAlphabeticalString() - val file = forgeTempFile(fileName) - val data = forge.anAlphabeticalString() - file.writeText(data) - whenever(mockOrchestrator.getReadableFile(mutableSetOf())) doReturn file - whenever(mockOrchestrator.getReadableFile(mutableSetOf(fileName))) doReturn null - - val firstBatch = testedReader.readNextBatch() - val secondBatch = testedReader.readNextBatch() - checkNotNull(firstBatch) - testedReader.releaseBatch(firstBatch.id) - val thirdBatch = testedReader.readNextBatch() - checkNotNull(thirdBatch) - - assertThat(String(firstBatch.data)).isEqualTo("$fakePrefix$data$fakeSuffix") - assertThat(secondBatch).isNull() - assertThat(String(thirdBatch.data)).isEqualTo("$fakePrefix$data$fakeSuffix") - inOrder(mockOrchestrator) { - verify(mockOrchestrator).getReadableFile(emptySet()) - verify(mockOrchestrator).getReadableFile(setOf(firstBatch.id)) - verify(mockOrchestrator).getReadableFile(emptySet()) - verifyNoMoreInteractions(mockOrchestrator) - } - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `returns a valid batch if file exists and valid`( - forge: Forge - ) { - // Given - val fileName = forge.anAlphabeticalString() - val file = forgeTempFile(fileName) - val data = forge.anAlphabeticalString() - file.writeText(data) - whenever(mockOrchestrator.getReadableFile(any())).thenReturn(file) - - // When - val nextBatch = testedReader.readNextBatch() - checkNotNull(nextBatch) - - // Then - val persistedData = String(nextBatch.data) - assertThat(persistedData).isEqualTo("$fakePrefix$data$fakeSuffix") - } - - @Test - fun `returns a null batch if the file was already sent`() { - // Given - whenever(mockOrchestrator.getReadableFile(any())).doReturn(null) - - // When - val nextBatch = testedReader.readNextBatch() - - // Then - assertThat(nextBatch).isNull() - } - - @Test - fun `returns a null batch if the data is corrupted`() { - // Given - whenever(mockOrchestrator.getReadableFile(any())).doReturn(null) - - // When - val nextBatch = testedReader.readNextBatch() - - // Then - assertThat(nextBatch).isNull() - } - - @Test - fun `M return null W readNextBatch() and orchestrator throws SecurityException`( - forge: Forge - ) { - // Given - val exception = SecurityException(forge.anAlphabeticalString()) - doThrow(exception).whenever(mockOrchestrator).getReadableFile(any()) - - // When - val nextBatch = testedReader.readNextBatch() - - // Then - assertThat(nextBatch).isNull() - } - - @Test - fun `M returns null W readNextBatch() and orchestrator throws OutOfMemoryError`( - forge: Forge - ) { - // Given - val exception = OutOfMemoryError(forge.anAlphabeticalString()) - doThrow(exception).whenever(mockOrchestrator).getReadableFile(any()) - - // When - val nextBatch = testedReader.readNextBatch() - - // Then - assertThat(nextBatch).isNull() - } - - @Test - fun `M returns empty batch W readNextBatch() and file doesn't exist`( - @StringForgery dirName: String, - @StringForgery fileName: String, - forge: Forge - ) { - // Given - val file = File(File(tempRootDir, dirName), fileName) - whenever(mockOrchestrator.getReadableFile(any())).thenReturn(file) - - // When - val nextBatch = testedReader.readNextBatch() - - // Then - assertThat(nextBatch?.data).isEmpty() - assertThat(nextBatch?.id).isEqualTo(file.name) - } - - @Test - fun `M drop the batch W dropBatch() and file exists`( - forge: Forge - ) { - // Given - val fileName = forge.anAlphabeticalString() - val file: File = forgeTempFile(fileName) - whenever(mockOrchestrator.getReadableFile(any())).thenReturn(file) - testedReader.readNextBatch() - - // Then - testedReader.dropBatch(fileName) - - // Then - val lockedFiles: MutableSet = testedReader.getFieldValue("lockedFiles") - assertThat(lockedFiles).isEmpty() - assertThat(tempRootDir.listFiles()).isEmpty() - } - - @Test - fun `M do nothing W dropBatch() and file doesn't exist`( - forge: Forge - ) { - // Given - val fileName = forge.anAlphabeticalString() - val file: File = forgeTempFile(fileName) - whenever(mockOrchestrator.getReadableFile(any())).thenReturn(file) - testedReader.readNextBatch() - val notExistingFileName = forge.anAlphabeticalString() - - // When - testedReader.dropBatch(notExistingFileName) - - // Then - val lockedFiles: MutableSet = testedReader.getFieldValue("lockedFiles") - assertThat(lockedFiles).containsOnly(fileName) - } - - @Test - fun `M clean root folder W dropAllBatches()`(forge: Forge) { - // Given - val fileName1 = forge.anAlphabeticalString() - val fileName2 = forge.anAlphabeticalString() - val file1 = forgeTempFile(fileName1) - val file2 = forgeTempFile(fileName2) - whenever(mockOrchestrator.getAllFiles()).thenReturn(arrayOf(file1, file2)) - - // When - testedReader.dropAllBatches() - - // Then - val lockedFiles: MutableSet = testedReader.getFieldValue("lockedFiles") - assertThat(tempRootDir.listFiles()).isEmpty() - assertThat(lockedFiles).isEmpty() - } - - @Test - fun `it will do nothing if the only available file to be sent is locked`(forge: Forge) { - // Given - val inProgressFileName = forge.anAlphabeticalString() - val inProgressFile = forgeTempFile(inProgressFileName) - val countDownLatch = CountDownLatch(2) - whenever(mockOrchestrator.getReadableFile(emptySet())) - .thenReturn(inProgressFile) - .thenReturn(null) - whenever(mockOrchestrator.getReadableFile(setOf(inProgressFileName))).thenReturn(null) - - var batch1: Batch? = null - var batch2: Batch? = null - - // When - Thread { - batch1 = testedReader.readNextBatch() - Thread { - batch2 = testedReader.readNextBatch() - countDownLatch.countDown() - }.start() - countDownLatch.countDown() - }.start() - - // Then - countDownLatch.await(5, TimeUnit.SECONDS) - assertThat(batch1?.id).isEqualTo(inProgressFileName) - assertThat(batch2).isNull() - } - - @Test - fun `it will return the next file if the current one is locked`(forge: Forge) { - // Given - val inProgressFileName = forge.anAlphabeticalString() - val nextFileName = inProgressFileName + "_next" - val inProgressFile = forgeTempFile(inProgressFileName) - val nextFile = forgeTempFile(nextFileName) - val countDownLatch = CountDownLatch(2) - whenever(mockOrchestrator.getReadableFile(emptySet())) - .thenReturn(inProgressFile) - .thenReturn(null) - whenever(mockOrchestrator.getReadableFile(setOf(inProgressFileName))).thenReturn(nextFile) - - var batch1: Batch? = null - var batch2: Batch? = null - - // When - Thread { - batch1 = testedReader.readNextBatch() - Thread { - batch2 = testedReader.readNextBatch() - countDownLatch.countDown() - }.start() - countDownLatch.countDown() - }.start() - - // Then - countDownLatch.await(5, TimeUnit.SECONDS) - assertThat(batch1?.id).isEqualTo(inProgressFileName) - assertThat(batch2?.id).isEqualTo(nextFileName) - } - - @Test - fun `it will return the released file`(forge: Forge) { - // Given - val inProgressFileName = forge.anAlphabeticalString() - val nextFileName = inProgressFileName + "_next" - val inProgressFile = forgeTempFile(inProgressFileName) - val nextFile = forgeTempFile(nextFileName) - val countDownLatch = CountDownLatch(2) - whenever(mockOrchestrator.getReadableFile(emptySet())) - .thenReturn(inProgressFile) - whenever(mockOrchestrator.getReadableFile(setOf(inProgressFileName))).thenReturn(nextFile) - - var batch2: Batch? = null - - // When - Thread { - val batch1 = testedReader.readNextBatch() - Thread { - Thread.sleep(500) // give timet o first thread to release the batch - batch2 = testedReader.readNextBatch() - countDownLatch.countDown() - }.start() - batch1?.let { - testedReader.releaseBatch(it.id) - } - countDownLatch.countDown() - }.start() - - // Then - countDownLatch.await(5, TimeUnit.SECONDS) - assertThat(batch2?.id).isEqualTo(inProgressFileName) - } - - @Test - fun `it will not throw exception in case of concurrent access`(forge: Forge) { - val file1 = forgeTempFile(forge.anAlphabeticalString()) - val file2 = forgeTempFile(forge.anAlphabeticalString()) - val file3 = forgeTempFile(forge.anAlphabeticalString()) - val file4 = forgeTempFile(forge.anAlphabeticalString()) - whenever(mockOrchestrator.getReadableFile(any())) - .thenReturn(file1) - .thenReturn(file2) - .thenReturn(file3) - .thenReturn(file4) - val countDownLatch = CountDownLatch(4) - repeat(4) { - Thread { - testedReader.readNextBatch()?.let { testedReader.releaseBatch(it.id) } - countDownLatch.countDown() - }.start() - } - - countDownLatch.await(5, TimeUnit.SECONDS) - } - - // region Internal - - private fun forgeTempFile(fileName: String): File { - val file = File(tempRootDir, fileName) - file.createNewFile() - return file - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/ImmediateFileWriterTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/ImmediateFileWriterTest.kt deleted file mode 100644 index 55fcd9f5bf..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/file/ImmediateFileWriterTest.kt +++ /dev/null @@ -1,261 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.file - -import android.os.Build -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.core.internal.domain.Serializer -import com.datadog.android.core.internal.threading.AndroidDeferredHandler -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class ImmediateFileWriterTest { - - lateinit var testedWriter: ImmediateFileWriter - - @Mock - lateinit var mockSerializer: Serializer - - @Mock - lateinit var mockOrchestrator: Orchestrator - - @Mock - lateinit var mockDeferredHandler: AndroidDeferredHandler - - @TempDir - lateinit var tempRootDir: File - - @BeforeEach - fun `set up`() { - whenever(mockSerializer.serialize(any())).doAnswer { - it.getArgument(0) - } - whenever(mockDeferredHandler.handle(any())) doAnswer { - val runnable = it.arguments[0] as Runnable - runnable.run() - } - testedWriter = ImmediateFileWriter( - mockOrchestrator, - mockSerializer - ) - } - - @AfterEach - fun `tear down`() { - tempRootDir.deleteRecursively() - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `𝕄 write a valid model 𝕎 write(model)`(forge: Forge) { - val model = forge.anAlphabeticalString() - val fileNameToWriteTo = forge.anAlphaNumericalString() - val file = File(tempRootDir, fileNameToWriteTo) - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(file) - - testedWriter.write(model) - - assertThat(file.readText()) - .isEqualTo(model) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `𝕄 write a collection of models 𝕎 write(list)`(forge: Forge) { - val models: List = forge.aList { forge.anAlphabeticalString() } - val fileNameToWriteTo = forge.anAlphaNumericalString() - val file = File(tempRootDir, fileNameToWriteTo) - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(file) - - testedWriter.write(models) - - assertThat(file.readText().split(",")) - .isEqualTo(models) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `𝕄 write several models 𝕎 write()+`(forge: Forge) { - val models = forge.aList { anAlphabeticalString() } - val fileNameToWriteTo = forge.anAlphaNumericalString() - val file = File(tempRootDir, fileNameToWriteTo) - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(file) - - models.forEach { - testedWriter.write(it) - } - - assertThat(file.readText()) - .isEqualTo(models.joinToString(",")) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `𝕄 write several models with custom separator 𝕎 write()+`(forge: Forge) { - val separator = forge.anAsciiString() - testedWriter = ImmediateFileWriter( - mockOrchestrator, - mockSerializer, - separator - ) - val models = forge.aList { anAlphabeticalString() } - val fileNameToWriteTo = forge.anAlphaNumericalString() - val file = File(tempRootDir, fileNameToWriteTo) - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(file) - - models.forEach { - testedWriter.write(it) - } - - assertThat(file.readText()) - .isEqualTo(models.joinToString(separator)) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `𝕄 do nothing 𝕎 write() and serialisation fails`( - @StringForgery model: String, - @StringForgery errorMessage: String - ) { - val throwable = RuntimeException(errorMessage) - doThrow(throwable).whenever(mockSerializer).serialize(model) - - testedWriter.write(model) - - verifyZeroInteractions(mockDeferredHandler) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `𝕄 do nothing 𝕎 write() with SecurityException thrown while providing a file`( - forge: Forge - ) { - val modelValue = forge.anAlphabeticalString() - val exception = SecurityException(forge.anAlphabeticalString()) - doThrow(exception).whenever(mockOrchestrator).getWritableFile(any()) - - testedWriter.write(modelValue) - - verifyZeroInteractions(mockDeferredHandler) - } - - @Test - fun `𝕄 do nothing 𝕎 write() and FileOrchestrator returns a null file`( - forge: Forge - ) { - val modelValue = forge.anAlphabeticalString() - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(null) - - // When - testedWriter.write(modelValue) - - // Then - verifyZeroInteractions(mockDeferredHandler) - } - - @Test - fun `𝕄 do nothing 𝕎 write() and FileOrchestrator returns a file that doesn't exist`( - @StringForgery dirName: String, - @StringForgery fileName: String, - forge: Forge - ) { - val nonExistentDir = File(tempRootDir, dirName) - val file = File(nonExistentDir, fileName) - val modelValue = forge.anAlphabeticalString() - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(file) - - // When - testedWriter.write(modelValue) - - // Then - verifyZeroInteractions(mockDeferredHandler) - } - - @Test - fun `𝕄 respect file locks 𝕎 write() on locked file`( - forge: Forge - ) { - val models = forge.aList { anAlphabeticalString() } - val fileNameToWriteTo = forge.anAlphaNumericalString() - val file = File(tempRootDir, fileNameToWriteTo) - file.createNewFile() - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(file) - - val outputStream = file.outputStream() - val lock = outputStream.channel.lock() - try { - models.forEach { - testedWriter.write(it) - } - } finally { - lock.release() - outputStream.close() - } - - assertThat(file.readText()) - .isEmpty() - } - - @Test - fun `𝕄 lock and release file 𝕎 write() from multiple threads`(forge: Forge) { - val models = forge.aList(size = 10) { anAlphabeticalString() } - val fileNameToWriteTo = forge.anAlphaNumericalString() - val file = File(tempRootDir, fileNameToWriteTo) - file.createNewFile() - whenever(mockOrchestrator.getWritableFile(any())).thenReturn(file) - val countDownLatch = CountDownLatch(2) - - Thread { - models.take(5).forEach { - testedWriter.write(it) - } - countDownLatch.countDown() - }.start() - - Thread { - models.takeLast(5).forEach { - testedWriter.write(it) - } - countDownLatch.countDown() - }.start() - - countDownLatch.await(4, TimeUnit.SECONDS) - assertThat(file.readText().split(",").size) - .isEqualTo(models.size) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/privacy/TrackingConsentProviderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/privacy/TrackingConsentProviderTest.kt deleted file mode 100644 index 9b988ab409..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/privacy/TrackingConsentProviderTest.kt +++ /dev/null @@ -1,207 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.privacy - -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.privacy.TrackingConsentProviderCallback -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.argForWhich -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.assertj.core.api.Java6Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class TrackingConsentProviderTest { - - lateinit var testedConsentProvider: TrackingConsentProvider - - @Mock - lateinit var mockedCallback: TrackingConsentProviderCallback - - @BeforeEach - fun `set up`() { - testedConsentProvider = TrackingConsentProvider(TrackingConsent.PENDING) - } - - @Test - fun `M hold PENDING consent by default W initialised`(forge: Forge) { - assertThat(testedConsentProvider.getConsent()).isEqualTo(TrackingConsent.PENDING) - } - - @Test - fun `M update last consent W required`(forge: Forge) { - // GIVEN - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - - // WHEN - testedConsentProvider.setConsent(fakeConsent) - - // THEN - assertThat(testedConsentProvider.getConsent()).isEqualTo(fakeConsent) - } - - @Test - fun `M notify callbacks W updating consent`(forge: Forge) { - // GIVEN - val fakeConsent = - forge.aValueFrom(TrackingConsent::class.java, listOf(TrackingConsent.PENDING)) - testedConsentProvider.registerCallback(mockedCallback) - - // WHEN - testedConsentProvider.setConsent(fakeConsent) - - // THEN - verify(mockedCallback).onConsentUpdated(TrackingConsent.PENDING, fakeConsent) - verifyNoMoreInteractions(mockedCallback) - } - - @Test - fun `M not notify callbacks W updating consent with same value`(forge: Forge) { - // GIVEN - val fakeConsent = - forge.aValueFrom(TrackingConsent::class.java, listOf(TrackingConsent.PENDING)) - testedConsentProvider.registerCallback(mockedCallback) - testedConsentProvider.setConsent(fakeConsent) - - // WHEN - testedConsentProvider.setConsent(fakeConsent) - - // THEN - verify(mockedCallback).onConsentUpdated(TrackingConsent.PENDING, fakeConsent) - verifyNoMoreInteractions(mockedCallback) - } - - @Test - fun `M unregister all callbacks W requested`(forge: Forge) { - // GIVEN - val fakeConsent = - forge.aValueFrom(TrackingConsent::class.java, listOf(TrackingConsent.PENDING)) - val anotherMockedCallback: TrackingConsentProviderCallback = mock() - testedConsentProvider.registerCallback(anotherMockedCallback) - testedConsentProvider.registerCallback(mockedCallback) - - // WHEN - testedConsentProvider.unregisterAllCallbacks() - testedConsentProvider.setConsent(fakeConsent) - - // THEN - verifyZeroInteractions(mockedCallback) - verifyZeroInteractions(anotherMockedCallback) - } - - @Test - fun `M unregister first W called asynchronously`(forge: Forge) { - // GIVEN - val fakeConsent = forge.aValueFrom( - TrackingConsent::class.java, - listOf(TrackingConsent.PENDING) - ) - testedConsentProvider.registerCallback(mockedCallback) - val countDownLatch = CountDownLatch(2) - - // WHEN - Thread { - testedConsentProvider.unregisterAllCallbacks() - countDownLatch.countDown() - }.start() - Thread { - Thread.sleep(1) - testedConsentProvider.setConsent(fakeConsent) - countDownLatch.countDown() - }.start() - - // THEN - verifyZeroInteractions(mockedCallback) - } - - @Test - fun `M always return the right value W updating from multiple threads`(forge: Forge) { - // GIVEN - val fakedConsent1 = forge.aValueFrom(TrackingConsent::class.java) - val fakedConsent2 = - forge.aValueFrom(TrackingConsent::class.java, listOf(TrackingConsent.PENDING)) - val countDownLatch = CountDownLatch(2) - - // WHEN - Thread { - testedConsentProvider.setConsent(fakedConsent1) - countDownLatch.countDown() - }.start() - Thread { - Thread.sleep(10) // just to give time to the first thread - testedConsentProvider.setConsent(fakedConsent2) - countDownLatch.countDown() - }.start() - countDownLatch.await(1, TimeUnit.SECONDS) - - // THEN - assertThat(testedConsentProvider.getConsent()).isEqualTo(fakedConsent2) - } - - @Test - fun `M notify the registered callback W registering from different threads`(forge: Forge) { - // GIVEN - val fakeConsent1 = TrackingConsent.GRANTED - val fakeConsent2 = TrackingConsent.NOT_GRANTED - val countDownLatch = CountDownLatch(3) - - // WHEN - Thread { - testedConsentProvider.registerCallback(mockedCallback) - countDownLatch.countDown() - }.start() - Thread { - Thread.sleep(2) // just to callback register thread to take the lock - testedConsentProvider.setConsent(fakeConsent1) - countDownLatch.countDown() - }.start() - Thread { - Thread.sleep(2) - testedConsentProvider.setConsent(fakeConsent2) - countDownLatch.countDown() - }.start() - countDownLatch.await(1, TimeUnit.SECONDS) - - // THEN - assertThat(testedConsentProvider.getConsent()).isIn(fakeConsent1, fakeConsent2) - verify(mockedCallback).onConsentUpdated( - argForWhich { - this == TrackingConsent.PENDING || this == fakeConsent2 - }, - eq(fakeConsent1) - ) - verify(mockedCallback).onConsentUpdated( - argForWhich { - this == TrackingConsent.PENDING || this == fakeConsent1 - }, - eq(fakeConsent2) - ) - verifyNoMoreInteractions(mockedCallback) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnableTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnableTest.kt deleted file mode 100644 index 8d93faf31b..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadRunnableTest.kt +++ /dev/null @@ -1,522 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.upload - -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.data.file.Batch -import com.datadog.android.core.internal.net.DataUploader -import com.datadog.android.core.internal.net.UploadStatus -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.system.SystemInfo -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.same -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.TimeUnit -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DataUploadRunnableTest { - - @Mock - lateinit var mockThreadPoolExecutor: ScheduledThreadPoolExecutor - - @Mock - lateinit var mockReader: Reader - - @Mock - lateinit var mockDataUploader: DataUploader - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockSystemInfoProvider: SystemInfoProvider - - lateinit var testedRunnable: DataUploadRunnable - - @BeforeEach - fun `set up`(forge: Forge) { - val fakeNetworkInfo = - NetworkInfo( - forge.aValueFrom( - enumClass = NetworkInfo.Connectivity::class.java, - exclude = listOf(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - ) - ) - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo - val fakeSystemInfo = SystemInfo( - batteryStatus = forge.aValueFrom(SystemInfo.BatteryStatus::class.java), - batteryLevel = forge.anInt(20, 100) - ) - whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn fakeSystemInfo - - testedRunnable = - DataUploadRunnable( - mockThreadPoolExecutor, - mockReader, - mockDataUploader, - mockNetworkInfoProvider, - mockSystemInfoProvider - ) - } - - @Test - fun `doesn't send batch when offline`(@Forgery batch: Batch) { - val networkInfo = - NetworkInfo( - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED - ) - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn networkInfo - - testedRunnable.run() - - verify(mockReader, never()).dropBatch(batch.id) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader, never()).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `doesn't send batch when battery is low and unplugged`( - @Forgery batch: Batch, - forge: Forge - ) { - val systemInfo = SystemInfo( - forge.anElementFrom( - SystemInfo.BatteryStatus.DISCHARGING, - SystemInfo.BatteryStatus.NOT_CHARGING - ), - forge.anInt(1, 10) - ) - whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn systemInfo - whenever(mockReader.readNextBatch()) doReturn batch - - testedRunnable.run() - - verify(mockReader, never()).dropBatch(anyOrNull()) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader, never()).upload(anyOrNull()) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `doesn't send batch when power save mode is enabled`( - @Forgery batch: Batch, - forge: Forge - ) { - val systemInfo = SystemInfo( - batteryStatus = forge.anElementFrom( - SystemInfo.BatteryStatus.DISCHARGING, - SystemInfo.BatteryStatus.NOT_CHARGING - ), - batteryLevel = forge.anInt(50, 100), - powerSaveMode = true - ) - whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn systemInfo - whenever(mockReader.readNextBatch()) doReturn batch - - testedRunnable.run() - - verify(mockReader, never()).dropBatch(anyOrNull()) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader, never()).upload(anyOrNull()) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch sent when battery is low and charging`( - @Forgery batch: Batch, - forge: Forge - ) { - val systemInfo = SystemInfo( - SystemInfo.BatteryStatus.CHARGING, - forge.anInt(1, 10) - ) - whenever(mockSystemInfoProvider.getLatestSystemInfo()) doReturn systemInfo - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.SUCCESS - - testedRunnable.run() - - verify(mockReader).dropBatch(batch.id) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `𝕄 do nothing 𝕎 no batch to send`() { - whenever(mockReader.readNextBatch()) doReturn null - - testedRunnable.run() - - verify(mockReader, never()).dropBatch(anyOrNull()) - verify(mockReader, never()).releaseBatch(anyOrNull()) - verifyZeroInteractions(mockDataUploader) - verify(mockThreadPoolExecutor).schedule( - eq(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch sent successfully`(@Forgery batch: Batch) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.SUCCESS - - testedRunnable.run() - - verify(mockReader).dropBatch(batch.id) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch kept on Network Error`(@Forgery batch: Batch) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.NETWORK_ERROR - - testedRunnable.run() - - verify(mockReader, never()).dropBatch(batch.id) - verify(mockReader).releaseBatch(batch.id) - verify(mockDataUploader).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch kept after n Network Error`( - @Forgery batch: Batch, - @IntForgery(min = 3, max = 42) runCount: Int - ) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.NETWORK_ERROR - - for (i in 0 until runCount) { - testedRunnable.run() - } - verify(mockDataUploader, times(runCount)).upload(batch.data) - verify(mockReader, never()).dropBatch(batch.id) - verify(mockReader, times(runCount)).releaseBatch(batch.id) - verify(mockThreadPoolExecutor, times(runCount)).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch dropped on Redirection`(@Forgery batch: Batch) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.HTTP_REDIRECTION - - testedRunnable.run() - - verify(mockReader).dropBatch(batch.id) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch dropped on Client Error`(@Forgery batch: Batch) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.HTTP_CLIENT_ERROR - - testedRunnable.run() - - verify(mockReader).dropBatch(batch.id) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch kept on Server Error`(@Forgery batch: Batch) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.HTTP_SERVER_ERROR - - testedRunnable.run() - - verify(mockReader, never()).dropBatch(batch.id) - verify(mockReader).releaseBatch(batch.id) - verify(mockDataUploader).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch kept after n Server Error`( - @Forgery batch: Batch, - @IntForgery(min = 3, max = 42) runCount: Int - ) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.HTTP_SERVER_ERROR - - for (i in 0 until runCount) { - testedRunnable.run() - } - - verify(mockDataUploader, times(runCount)).upload(batch.data) - verify(mockReader, never()).dropBatch(batch.id) - verify(mockReader, times(runCount)).releaseBatch(batch.id) - verify(mockThreadPoolExecutor, times(runCount)).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `batch dropped on Unknown error`(@Forgery batch: Batch) { - whenever(mockReader.readNextBatch()) doReturn batch - whenever(mockDataUploader.upload(batch.data)) doReturn UploadStatus.UNKNOWN_ERROR - - testedRunnable.run() - - verify(mockReader).dropBatch(batch.id) - verify(mockReader, never()).releaseBatch(batch.id) - verify(mockDataUploader).upload(batch.data) - verify(mockThreadPoolExecutor).schedule( - same(testedRunnable), - any(), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `when has batches the upload frequency will increase`( - @Forgery batch: Batch - ) { - whenever(mockDataUploader.upload(any())) doReturn UploadStatus.SUCCESS - whenever(mockReader.readNextBatch()).doReturn(batch) - - repeat(5) { - testedRunnable.run() - } - - val captor = argumentCaptor() - verify(mockThreadPoolExecutor, times(5)) - .schedule(same(testedRunnable), captor.capture(), eq(TimeUnit.MILLISECONDS)) - captor.allValues.reduce { previous, next -> - assertThat(next).isLessThan(previous) - next - } - } - - @Test - fun `𝕄 reduce delay between runs 𝕎 upload is successful`( - @Forgery batch: Batch, - @IntForgery(16, 64) runCount: Int - ) { - // Given - whenever(mockDataUploader.upload(any())) doReturn UploadStatus.SUCCESS - whenever(mockReader.readNextBatch()).doReturn(batch) - - // When - repeat(runCount) { - testedRunnable.run() - } - - // Then - argumentCaptor { - verify(mockThreadPoolExecutor, times(runCount)) - .schedule( - same(testedRunnable), - capture(), - eq(TimeUnit.MILLISECONDS) - ) - - allValues.reduce { previous, next -> - assertThat(next) - .isLessThanOrEqualTo(previous) - .isBetween(DataUploadRunnable.MIN_DELAY_MS, DataUploadRunnable.MAX_DELAY_MS) - next - } - } - } - - @Test - fun `𝕄 reduce delay between runs 𝕎 batch fails and should be dropped`( - @Forgery batch: Batch, - @IntForgery(16, 64) runCount: Int, - forge: Forge - ) { - // Given - whenever(mockDataUploader.upload(any())) doAnswer { - forge.anElementFrom( - UploadStatus.HTTP_REDIRECTION, - UploadStatus.HTTP_CLIENT_ERROR, - UploadStatus.UNKNOWN_ERROR - ) - } - whenever(mockReader.readNextBatch()).doReturn(batch) - - // When - repeat(runCount) { - testedRunnable.run() - } - - // Then - argumentCaptor { - verify(mockThreadPoolExecutor, times(runCount)) - .schedule( - same(testedRunnable), - capture(), - eq(TimeUnit.MILLISECONDS) - ) - - allValues.reduce { previous, next -> - assertThat(next) - .isLessThanOrEqualTo(previous) - .isBetween(DataUploadRunnable.MIN_DELAY_MS, DataUploadRunnable.MAX_DELAY_MS) - next - } - } - } - - @Test - fun `𝕄 increase delay between runs 𝕎 no batch available`( - @IntForgery(16, 64) runCount: Int - ) { - // Given - whenever(mockDataUploader.upload(any())) doReturn UploadStatus.SUCCESS - whenever(mockReader.readNextBatch()) doReturn null - - // When - repeat(runCount) { - testedRunnable.run() - } - - // Then - argumentCaptor { - verify(mockThreadPoolExecutor, times(runCount)) - .schedule( - same(testedRunnable), - capture(), - eq(TimeUnit.MILLISECONDS) - ) - - allValues.reduce { previous, next -> - assertThat(next) - .isGreaterThanOrEqualTo(previous) - .isBetween(DataUploadRunnable.MIN_DELAY_MS, DataUploadRunnable.MAX_DELAY_MS) - next - } - } - } - - @Test - fun `𝕄 increase delay between runs 𝕎 batch fails and should be retried`( - @IntForgery(16, 64) runCount: Int, - forge: Forge - ) { - // Given - whenever(mockDataUploader.upload(any())) doAnswer { - forge.aValueFrom( - UploadStatus::class.java, - exclude = listOf( - UploadStatus.SUCCESS, - UploadStatus.HTTP_REDIRECTION, - UploadStatus.HTTP_CLIENT_ERROR, - UploadStatus.UNKNOWN_ERROR - ) - ) - } - whenever(mockReader.readNextBatch()) doReturn null - - // When - repeat(runCount) { - testedRunnable.run() - } - - // Then - argumentCaptor { - verify(mockThreadPoolExecutor, times(runCount)) - .schedule( - same(testedRunnable), - capture(), - eq(TimeUnit.MILLISECONDS) - ) - - allValues.reduce { previous, next -> - assertThat(next) - .isGreaterThanOrEqualTo(previous) - .isBetween(DataUploadRunnable.MIN_DELAY_MS, DataUploadRunnable.MAX_DELAY_MS) - next - } - } - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadSchedulerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadSchedulerTest.kt deleted file mode 100644 index 84a2c2f367..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/DataUploadSchedulerTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.upload - -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class DataUploadSchedulerTest { - - lateinit var testedScheduler: DataUploadScheduler - - @Mock - lateinit var mockExecutor: ScheduledThreadPoolExecutor - - @BeforeEach - fun `set up`() { - testedScheduler = DataUploadScheduler( - mock(), - mock(), - mock(), - mock(), - mockExecutor - ) - } - - @Test - fun `when start it will schedule a runnable`() { - // When - testedScheduler.startScheduling() - - // Then - verify(mockExecutor).schedule( - any(), - eq(DataUploadRunnable.DEFAULT_DELAY_MS), - eq(TimeUnit.MILLISECONDS) - ) - } - - @Test - fun `when stop it will try to remove the scheduled runnable`() { - // Given - testedScheduler.startScheduling() - - // When - testedScheduler.stopScheduling() - - // Then - val argumentCaptor = argumentCaptor() - verify(mockExecutor).schedule( - argumentCaptor.capture(), - eq(DataUploadRunnable.DEFAULT_DELAY_MS), - eq(TimeUnit.MILLISECONDS) - ) - verify(mockExecutor).remove(argumentCaptor.firstValue) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadWorkerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadWorkerTest.kt deleted file mode 100644 index 3634485110..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/data/upload/UploadWorkerTest.kt +++ /dev/null @@ -1,392 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.data.upload - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.data.file.Batch -import com.datadog.android.core.internal.domain.PersistenceStrategy -import com.datadog.android.core.internal.net.UploadStatus -import com.datadog.android.error.internal.CrashReportsFeature -import com.datadog.android.log.internal.LogsFeature -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.net.LogsOkHttpUploader -import com.datadog.android.tracing.internal.TracesFeature -import com.datadog.android.tracing.internal.net.TracesOkHttpUploader -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.opentracing.DDSpan -import com.datadog.tools.unit.invokeMethod -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assumptions.assumeTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class UploadWorkerTest { - - lateinit var testedWorker: Worker - - @Mock - lateinit var mockContext: Context - - @Mock - lateinit var mockLogsStrategy: PersistenceStrategy - - @Mock - lateinit var mockTracesStrategy: PersistenceStrategy - - @Mock - lateinit var mockCrashReportsStrategy: PersistenceStrategy - - @Mock - lateinit var mockLogsReader: Reader - - @Mock - lateinit var mockTracesReader: Reader - - @Mock - lateinit var mockCrashReportsReader: Reader - - @Mock - lateinit var mockLogsUploader: LogsOkHttpUploader - - @Mock - lateinit var mockTracesUploader: TracesOkHttpUploader - - @Mock - lateinit var mockCrashReportsUploader: LogsOkHttpUploader - - @Forgery - lateinit var fakeWorkerParameters: WorkerParameters - - @BeforeEach - fun `set up`() { - whenever(mockLogsStrategy.getReader()) doReturn mockLogsReader - whenever(mockTracesStrategy.getReader()) doReturn mockTracesReader - whenever(mockCrashReportsStrategy.getReader()) doReturn mockCrashReportsReader - - mockContext = mockContext() - Datadog.initialize( - mockContext, - DatadogConfig.Builder("CLIENT_TOKEN", "ENVIRONMENT").build() - ) - - LogsFeature.persistenceStrategy = mockLogsStrategy - LogsFeature.uploader = mockLogsUploader - TracesFeature.persistenceStrategy = mockTracesStrategy - TracesFeature.uploader = mockTracesUploader - CrashReportsFeature.persistenceStrategy = mockCrashReportsStrategy - CrashReportsFeature.uploader = mockCrashReportsUploader - - testedWorker = UploadWorker( - mockContext, - fakeWorkerParameters - ) - } - - @AfterEach - fun `tear down`() { - Datadog.invokeMethod("stop") - } - - @Test - fun `doWork single batch Success`( - @Forgery logsBatch: Batch, - @Forgery tracesBatch: Batch, - @Forgery crashReportsBatch: Batch - ) { - whenever(mockLogsReader.readNextBatch()).doReturn(logsBatch, null) - whenever(mockLogsUploader.upload(logsBatch.data)) doReturn UploadStatus.SUCCESS - whenever(mockTracesReader.readNextBatch()).doReturn(tracesBatch, null) - whenever(mockTracesUploader.upload(tracesBatch.data)) doReturn UploadStatus.SUCCESS - whenever(mockCrashReportsReader.readNextBatch()).doReturn(crashReportsBatch, null) - whenever(mockCrashReportsUploader.upload(crashReportsBatch.data)) - .doReturn(UploadStatus.SUCCESS) - - val result = testedWorker.doWork() - - verify(mockLogsReader).dropBatch(logsBatch.id) - verify(mockLogsReader, never()).releaseBatch(logsBatch.id) - verify(mockTracesReader).dropBatch(tracesBatch.id) - verify(mockTracesReader, never()).releaseBatch(tracesBatch.id) - verify(mockCrashReportsReader).dropBatch(crashReportsBatch.id) - verify(mockCrashReportsReader, never()).releaseBatch(crashReportsBatch.id) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } - - @Test - fun `doWork single batch Failure`( - @Forgery logsBatch: Batch, - @Forgery tracesBatch: Batch, - @Forgery crashReportsBatch: Batch, - forge: Forge - ) { - val status = forge.aValueFrom( - UploadStatus::class.java, - exclude = listOf(UploadStatus.SUCCESS) - ) - whenever(mockLogsReader.readNextBatch()).doReturn(logsBatch, null) - whenever(mockLogsUploader.upload(logsBatch.data)) doReturn status - whenever(mockTracesReader.readNextBatch()).doReturn(tracesBatch, null) - whenever(mockTracesUploader.upload(tracesBatch.data)) doReturn status - whenever(mockCrashReportsReader.readNextBatch()).doReturn(crashReportsBatch, null) - whenever(mockCrashReportsUploader.upload(crashReportsBatch.data)) doReturn status - - val result = testedWorker.doWork() - - verify(mockLogsReader, never()).dropBatch(logsBatch.id) - verify(mockLogsReader).releaseBatch(logsBatch.id) - verify(mockTracesReader, never()).dropBatch(tracesBatch.id) - verify(mockTracesReader).releaseBatch(tracesBatch.id) - verify(mockCrashReportsReader, never()).dropBatch(crashReportsBatch.id) - verify(mockCrashReportsReader).releaseBatch(crashReportsBatch.id) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } - - @Test - fun `doWork multiple logs batches all Success`( - @Forgery batches: List - ) { - assumeTrue { - // make sure there are no id duplicates - batches.map { it.id }.toSet().size == batches.size - } - val firstBatch = batches.first() - val otherBatchesThenNull = Array(batches.size) { - batches.getOrNull(it + 1) - } - whenever(mockLogsReader.readNextBatch()).doReturn(firstBatch, *otherBatchesThenNull) - batches.forEach { - whenever(mockLogsUploader.upload(it.data)) doReturn UploadStatus.SUCCESS - } - - val result = testedWorker.doWork() - - batches.forEach { - verify(mockLogsReader).dropBatch(it.id) - verify(mockLogsReader, never()).releaseBatch(it.id) - } - verify(mockTracesReader, never()).dropBatch(any()) - verify(mockTracesReader, never()).releaseBatch(any()) - verify(mockCrashReportsReader, never()).dropBatch(any()) - verify(mockCrashReportsReader, never()).releaseBatch(any()) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } - - @Test - fun `doWork multiple traces batches all Success`( - @Forgery batches: List - ) { - assumeTrue { - // make sure there are no id duplicates - batches.map { it.id }.toSet().size == batches.size - } - val firstBatch = batches.first() - val otherBatchesThenNull = Array(batches.size) { - batches.getOrNull(it + 1) - } - whenever(mockTracesReader.readNextBatch()).doReturn(firstBatch, *otherBatchesThenNull) - batches.forEach { - whenever(mockTracesUploader.upload(it.data)) doReturn UploadStatus.SUCCESS - } - - val result = testedWorker.doWork() - - batches.forEach { - verify(mockTracesReader).dropBatch(it.id) - verify(mockTracesReader, never()).releaseBatch(it.id) - } - verify(mockLogsReader, never()).dropBatch(any()) - verify(mockLogsReader, never()).releaseBatch(any()) - verify(mockCrashReportsReader, never()).dropBatch(any()) - verify(mockCrashReportsReader, never()).releaseBatch(any()) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } - - @Test - fun `doWork multiple crashReports batches all Success`( - @Forgery batches: List - ) { - assumeTrue { - // make sure there are no id duplicates - batches.map { it.id }.toSet().size == batches.size - } - val firstBatch = batches.first() - val otherBatchesThenNull = Array(batches.size) { - batches.getOrNull(it + 1) - } - whenever(mockCrashReportsReader.readNextBatch()) - .doReturn(firstBatch, *otherBatchesThenNull) - batches.forEach { - whenever(mockCrashReportsUploader.upload(it.data)) doReturn UploadStatus.SUCCESS - } - - val result = testedWorker.doWork() - - batches.forEach { - verify(mockCrashReportsReader).dropBatch(it.id) - verify(mockCrashReportsReader, never()).releaseBatch(it.id) - } - verify(mockLogsReader, never()).dropBatch(any()) - verify(mockLogsReader, never()).releaseBatch(any()) - verify(mockTracesReader, never()).dropBatch(any()) - verify(mockTracesReader, never()).releaseBatch(any()) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } - - @Test - fun `doWork multiple logs batches first Failed`( - @Forgery batches: List, - forge: Forge - ) { - assumeTrue { - // make sure there are no id duplicates - batches.map { it.id }.toSet().size == batches.size - } - val status = forge.aValueFrom( - UploadStatus::class.java, - exclude = listOf(UploadStatus.SUCCESS) - ) - val firstBatch = batches.first() - val otherBatchesThenNull = Array(batches.size) { - batches.getOrNull(it + 1) - } - whenever(mockLogsReader.readNextBatch()).doReturn(firstBatch, *otherBatchesThenNull) - whenever(mockLogsUploader.upload(any())) doReturn UploadStatus.SUCCESS - whenever(mockLogsUploader.upload(firstBatch.data)) doReturn status - - val result = testedWorker.doWork() - - batches.forEach { - if (it == firstBatch) { - verify(mockLogsReader, never()).dropBatch(it.id) - verify(mockLogsReader).releaseBatch(it.id) - } else { - verify(mockLogsReader).dropBatch(it.id) - verify(mockLogsReader, never()).releaseBatch(it.id) - } - } - verify(mockTracesReader, never()).dropBatch(any()) - verify(mockTracesReader, never()).releaseBatch(any()) - verify(mockCrashReportsReader, never()).dropBatch(any()) - verify(mockCrashReportsReader, never()).releaseBatch(any()) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } - - @Test - fun `doWork multiple traces batches first Failed`( - @Forgery batches: List, - forge: Forge - ) { - assumeTrue { - // make sure there are no id duplicates - batches.map { it.id }.toSet().size == batches.size - } - val status = forge.aValueFrom( - UploadStatus::class.java, - exclude = listOf(UploadStatus.SUCCESS) - ) - val firstBatch = batches.first() - val otherBatchesThenNull = Array(batches.size) { - batches.getOrNull(it + 1) - } - whenever(mockTracesReader.readNextBatch()).doReturn(firstBatch, *otherBatchesThenNull) - whenever(mockTracesUploader.upload(any())) doReturn UploadStatus.SUCCESS - whenever(mockTracesUploader.upload(firstBatch.data)) doReturn status - - val result = testedWorker.doWork() - - batches.forEach { - if (it == firstBatch) { - verify(mockTracesReader, never()).dropBatch(it.id) - verify(mockTracesReader).releaseBatch(it.id) - } else { - verify(mockTracesReader).dropBatch(it.id) - verify(mockTracesReader, never()).releaseBatch(it.id) - } - } - verify(mockLogsReader, never()).dropBatch(any()) - verify(mockLogsReader, never()).releaseBatch(any()) - verify(mockCrashReportsReader, never()).dropBatch(any()) - verify(mockCrashReportsReader, never()).releaseBatch(any()) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } - - @Test - fun `doWork multiple crashReports batches first Failed`( - @Forgery batches: List, - forge: Forge - ) { - assumeTrue { - // make sure there are no id duplicates - batches.map { it.id }.toSet().size == batches.size - } - val status = forge.aValueFrom( - UploadStatus::class.java, - exclude = listOf(UploadStatus.SUCCESS) - ) - val firstBatch = batches.first() - val otherBatchesThenNull = Array(batches.size) { - batches.getOrNull(it + 1) - } - whenever(mockCrashReportsReader.readNextBatch()).doReturn(firstBatch, *otherBatchesThenNull) - whenever(mockCrashReportsUploader.upload(any())) doReturn UploadStatus.SUCCESS - whenever(mockCrashReportsUploader.upload(firstBatch.data)) doReturn status - - val result = testedWorker.doWork() - - batches.forEach { - if (it == firstBatch) { - verify(mockCrashReportsReader, never()).dropBatch(it.id) - verify(mockCrashReportsReader).releaseBatch(it.id) - } else { - verify(mockCrashReportsReader).dropBatch(it.id) - verify(mockCrashReportsReader, never()).releaseBatch(it.id) - } - } - verify(mockLogsReader, never()).dropBatch(any()) - verify(mockLogsReader, never()).releaseBatch(any()) - verify(mockTracesReader, never()).dropBatch(any()) - verify(mockTracesReader, never()).releaseBatch(any()) - assertThat(result) - .isEqualTo(ListenableWorker.Result.success()) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/FilePersistenceStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/FilePersistenceStrategyTest.kt deleted file mode 100644 index 610f5f6c39..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/FilePersistenceStrategyTest.kt +++ /dev/null @@ -1,375 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -import android.content.Context -import android.os.Build -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.data.Reader -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.data.file.Batch -import com.datadog.android.utils.asJsonArray -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.lines -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.invokeMethod -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ExecutorService -import java.util.concurrent.TimeUnit -import kotlin.math.min -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings() -internal abstract class FilePersistenceStrategyTest( - val dataFolderName: String, - val maxMessagesPerPath: Int = MAX_MESSAGES_PER_BATCH, - val payloadDecoration: PayloadDecoration, - val modelClass: Class -) { - - lateinit var testedWriter: Writer - lateinit var testedReader: Reader - - @TempDir - lateinit var tempDir: File - - lateinit var mockContext: Context - - @Mock - lateinit var mockExecutorService: ExecutorService - // region Setup - - @BeforeEach - open fun `set up`(forge: Forge) { - mockContext = mockContext() - whenever(mockContext.filesDir) doReturn tempDir - whenever(mockExecutorService.submit(any())) doAnswer { - (it.arguments[0] as Runnable).run() - null - } - Datadog.initialize( - mockContext, - DatadogConfig.Builder(forge.anAlphabeticalString(), forge.anHexadecimalString()).build() - ) - val persistingStrategy = getStrategy() - - testedWriter = persistingStrategy.getWriter() - testedReader = persistingStrategy.getReader() - - setUp(testedWriter, testedReader) - } - - @AfterEach - fun `tear down`() { - Datadog.invokeMethod("stop") - } - - abstract fun getStrategy(): PersistenceStrategy - - abstract fun setUp(writer: Writer, reader: Reader) - - abstract fun waitForNextBatch() - - // endregion - - // region Writer Tests - - @Test - fun `writes full message as json`(forge: Forge) { - val fakeModel = forge.getForgery(modelClass) - testedWriter.write(fakeModel) - waitForNextBatch() - val batch = testedReader.readNextBatch()!! - val model = getBatchElements(batch).first() as JsonObject - assertJsonMatchesModel(model, fakeModel) - } - - @Test - fun `writes minimal model as json`(forge: Forge) { - val fakeModel = forge.getForgery(modelClass) - val minimalModel = forgeMinimalCopy(fakeModel) - testedWriter.write(minimalModel) - waitForNextBatch() - val batch = testedReader.readNextBatch()!! - val jsonObject = getBatchElements(batch).first() as JsonObject - assertJsonMatchesModel(jsonObject, minimalModel) - } - - @Test - fun `writes batch of models`(forge: Forge) { - val fakeModels = forge.aList { forge.getForgery(modelClass) } - val sentModels = mutableListOf() - val logCount = min(maxMessagesPerPath, fakeModels.size) - for (i in 0 until logCount) { - val model = fakeModels[i] - testedWriter.write(model) - sentModels.add(model) - } - waitForNextBatch() - val batch = testedReader.readNextBatch()!! - - val elements = getBatchElements(batch) - val batchCount = min(maxMessagesPerPath, elements.size) - for (i in 0 until batchCount) { - val jsonObject = elements[i].asJsonObject - assertJsonMatchesModel(jsonObject, sentModels[i]) - } - } - - @Test - fun `writes in new batch if delay passed`(forge: Forge) { - val fakeModel = forge.getForgery(modelClass) - val nextModel = forge.getForgery(modelClass) - testedWriter.write(fakeModel) - waitForNextBatch() - - testedWriter.write(nextModel) - val batch = testedReader.readNextBatch()!! - val jsonObject = getBatchElements(batch).first() as JsonObject - assertJsonMatchesModel(jsonObject, fakeModel) - } - - @Test - fun `writes batch of models from mutliple threads`(forge: Forge) { - val fakeModels = forge.aList { forge.getForgery(modelClass) } - val runnables = fakeModels.map { - Runnable { testedWriter.write(it) } - } - runnables.forEach { - Thread(it).start() - } - - waitForNextBatch() - waitForNextBatch() - val batch = testedReader.readNextBatch()!! - val elements = getBatchElements(batch) - elements.forEachIndexed { i, jsonElement -> - val jsonObject = jsonElement.asJsonObject - assertJsonContainsModels(jsonObject, fakeModels) - } - } - - @Test - fun `don't write model if size is too big`(forge: Forge) { - val bigModel = forgeHeavyModel(forge) - testedWriter.write(bigModel) - waitForNextBatch() - val batch = testedReader.readNextBatch() - - assertThat(batch) - .isNull() - } - - @Test - fun `limit the number of models per batch`(forge: Forge) { - val models = forge.aList(maxMessagesPerPath * 3) { - forgeLightModel(forge) - } - - models.forEach { testedWriter.write(it) } - waitForNextBatch() - val batch = testedReader.readNextBatch()!! - testedReader.dropBatch(batch.id) - waitForNextBatch() - val batch2 = testedReader.readNextBatch()!! - - val elements = getBatchElements(batch) - val elements2 = getBatchElements(batch2) - assertThat(elements.size) - .isEqualTo(maxMessagesPerPath) - assertThat(elements2.size) - .isEqualTo(maxMessagesPerPath) - elements.forEachIndexed { i, model -> - val jsonObject = model.asJsonObject - assertJsonMatchesModel(jsonObject, models[i]) - } - elements2.forEachIndexed { i, model -> - val jsonObject = model.asJsonObject - assertJsonMatchesModel(jsonObject, models[i + maxMessagesPerPath]) - } - } - - @Test - fun `read returns null when first batch is already sent`(forge: Forge) { - val fakeModel = forge.getForgery(modelClass) - testedWriter.write(fakeModel) - waitForNextBatch() - val batch = testedReader.readNextBatch() - checkNotNull(batch) - - testedReader.dropBatch(batch.id) - val batch2 = testedReader.readNextBatch() - - assertThat(batch2) - .isNull() - } - - @Test - fun `read returns null when first batch is too recent`(forge: Forge) { - val fakeModel = forge.getForgery(modelClass) - testedWriter.write(fakeModel) - val batch = testedReader.readNextBatch() - assertThat(batch) - .isNull() - } - - @Test - fun `read returns null when nothing was written`() { - - val batch = testedReader.readNextBatch() - assertThat(batch) - .isNull() - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `read returns null when drop all was called`( - forge: Forge - ) { - val firstModels = forge.aList { forge.getForgery(modelClass) } - val secondModels = forge.aList { forge.getForgery(modelClass) } - firstModels.forEach { testedWriter.write(it) } - waitForNextBatch() - secondModels.forEach { testedWriter.write(it) } - - testedReader.dropAllBatches() - val batch = testedReader.readNextBatch() - - assertThat(batch) - .isNull() - } - - @Test - fun `fails gracefully if sent batch with unknown id`( - forge: Forge - ) { - val batchId = forge.aNumericalString() - - testedReader.dropBatch(batchId) - } - - @Test - fun `reads null when batch already sent but the other thread is still trying to delete this`( - forge: Forge - ) { - val fakeModel = forge.getForgery(modelClass) - testedWriter.write(fakeModel) - waitForNextBatch() - val countDownLatch = CountDownLatch(2) - val batch = testedReader.readNextBatch() - var batch2: Batch? = Batch("", ByteArray(0)) - checkNotNull(batch) - - Thread { - testedReader.dropBatch(batch.id) - countDownLatch.countDown() - }.start() - Thread { - batch2 = testedReader.readNextBatch() - countDownLatch.countDown() - }.start() - - countDownLatch.await(5, TimeUnit.SECONDS) - assertThat(batch2) - .isNull() - } - - @Test - fun `reads null when batch already sent but was not able to delete the file`( - forge: Forge - ) { - val fakeModel = forge.getForgery(modelClass) - testedWriter.write(fakeModel) - waitForNextBatch() - val batch = testedReader.readNextBatch() - checkNotNull(batch) - // delete file before drop to simulate the "not able to delete" behaviour - File(tempDir, batch.id).delete() - testedReader.dropBatch(batch.id) - // generate the sent batch file again after dropBatch was called - File(tempDir, batch.id).createNewFile() - val batch2 = testedReader.readNextBatch() - assertThat(batch2).isNull() - } - - @Test - fun `it will create and share one single writer instance`() { - // Given - val persistenceStrategy = getStrategy() - val currentWriter = persistenceStrategy.getWriter() - - // Then - assertThat(persistenceStrategy.getWriter()).isSameAs(currentWriter) - } - - // endregion - - // region Abstract - - abstract fun forgeMinimalCopy(of: T): T - abstract fun forgeLightModel(forge: Forge): T - abstract fun forgeHeavyModel(forge: Forge): T - - abstract fun assertJsonContainsModels( - jsonObject: JsonObject, - models: List - ) - - abstract fun assertJsonMatchesModel( - jsonObject: JsonObject, - model: T - ) - - // endregion - - // region Internal - - private fun getBatchElements(batch: Batch): List { - if (payloadDecoration == PayloadDecoration.JSON_ARRAY_DECORATION) { - return batch.asJsonArray.toList() - } else { - return batch.lines.map { JsonParser.parseString(it) } - } - } - - // endregion - - companion object { - - const val MAX_BATCH_SIZE: Long = 128 * 1024 - const val MAX_MESSAGES_PER_BATCH: Int = 32 - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/TimeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/TimeTest.kt deleted file mode 100644 index ed34debdbc..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/TimeTest.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -internal class TimeTest { - - @Test - fun `creates Time with current millis and nanos`() { - - val startMs = System.currentTimeMillis() - val startNs = System.nanoTime() - - val time = Time() - - val endNs = System.nanoTime() - val endMs = System.currentTimeMillis() - - assertThat(time.timestamp).isBetween(startMs, endMs) - assertThat(time.nanoTime).isBetween(startNs, endNs) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/assertj/PersistenceStrategyAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/assertj/PersistenceStrategyAssert.kt deleted file mode 100644 index c581d84a25..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/assertj/PersistenceStrategyAssert.kt +++ /dev/null @@ -1,82 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.assertj - -import com.datadog.android.core.internal.data.file.FileOrchestrator -import com.datadog.android.core.internal.data.file.ImmediateFileWriter -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.FilePersistenceStrategy -import com.datadog.android.core.internal.domain.batching.DefaultConsentAwareDataWriter -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat - -internal class PersistenceStrategyAssert(actual: FilePersistenceStrategy) : - AbstractObjectAssert, FilePersistenceStrategy>( - actual, - PersistenceStrategyAssert::class.java - ) { - - fun hasIntermediateStorageFolder(folderPath: String): PersistenceStrategyAssert { - val absolutePath = actual.intermediateFileOrchestrator.rootDirectory.absolutePath - assertThat(absolutePath) - .overridingErrorMessage( - "Expected strategy to have intermediate folder " + - "$folderPath but was $absolutePath" - ) - .isEqualTo(folderPath) - return this - } - - fun hasAuthorizedStorageFolder(folderPath: String): PersistenceStrategyAssert { - val absolutePath = actual.authorizedFileOrchestrator.rootDirectory.absolutePath - assertThat(absolutePath) - .overridingErrorMessage( - "Expected strategy to have authorized folder " + - "$folderPath but was $absolutePath" - ) - .isEqualTo(folderPath) - return this - } - - fun usesConsentAwareAsyncWriter(): PersistenceStrategyAssert { - assertThat(actual.getWriter()) - .isInstanceOf(DefaultConsentAwareDataWriter::class.java) - return this - } - - fun usesImmediateWriter(): PersistenceStrategyAssert { - assertThat(actual.getWriter()) - .isInstanceOf(ImmediateFileWriter::class.java) - return this - } - - fun hasConfig(config: FilePersistenceConfig): PersistenceStrategyAssert { - assertThat(actual.intermediateFileOrchestrator.filePersistenceConfig) - .isEqualToComparingFieldByField(config) - assertThat(actual.authorizedFileOrchestrator.filePersistenceConfig) - .isEqualToComparingFieldByField(config) - return this - } - - fun uploadsFrom(folderPath: String): PersistenceStrategyAssert { - val absolutePath = - (actual.fileReader.fileOrchestrator as FileOrchestrator).rootDirectory.absolutePath - assertThat(absolutePath) - .overridingErrorMessage( - "Expected strategy to upload from " + - "$folderPath but was uploading from $absolutePath" - ) - .isEqualTo(folderPath) - return this - } - - companion object { - internal fun assertThat(actual: FilePersistenceStrategy): - PersistenceStrategyAssert = - PersistenceStrategyAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DataProcessorFactoryTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DataProcessorFactoryTest.kt deleted file mode 100644 index b233c07fa9..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DataProcessorFactoryTest.kt +++ /dev/null @@ -1,113 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.core.internal.data.file.ImmediateFileWriter -import com.datadog.android.core.internal.domain.Serializer -import com.datadog.android.core.internal.domain.batching.processors.DefaultDataProcessor -import com.datadog.android.core.internal.domain.batching.processors.NoOpDataProcessor -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.ExecutorService -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) -internal class DataProcessorFactoryTest { - - lateinit var testedFactory: DataProcessorFactory - - @Mock - lateinit var mockedExecutorService: ExecutorService - - @Mock - lateinit var mockedSerializer: Serializer - - @Mock - lateinit var mockedIntermediateFileOrchestrator: Orchestrator - - @Mock - lateinit var mockedTargetFileOrchestrator: Orchestrator - - lateinit var fakeEventsSeparator: CharSequence - - @BeforeEach - fun `set up`(forge: Forge) { - fakeEventsSeparator = forge.aString(size = 2) - testedFactory = DataProcessorFactory( - mockedIntermediateFileOrchestrator, - mockedTargetFileOrchestrator, - mockedSerializer, - fakeEventsSeparator, - mockedExecutorService - - ) - } - - @Test - fun `M use provide the appropriate processor W consent { PENDING }`() { - // WHEN - val processor = testedFactory.resolveProcessor(TrackingConsent.PENDING) - - // THEN - assertThat(processor).isInstanceOfSatisfying(DefaultDataProcessor::class.java) { - val immediateFileWriter = it.getWriter() as ImmediateFileWriter - assertThat(immediateFileWriter.fileOrchestrator) - .isEqualTo(mockedIntermediateFileOrchestrator) - } - } - - @Test - fun `M reset the intermediateOrchestrator W consent { PENDING }`() { - // WHEN - testedFactory.resolveProcessor(TrackingConsent.PENDING) - - // THEN - verify(mockedIntermediateFileOrchestrator).reset() - verifyNoMoreInteractions(mockedIntermediateFileOrchestrator) - } - - @Test - fun `M use provide the appropriate processor W consent { GRANTED }`() { - // WHEN - val processor = testedFactory.resolveProcessor(TrackingConsent.GRANTED) - - // THEN - assertThat(processor).isInstanceOfSatisfying(DefaultDataProcessor::class.java) { - val immediateFileWriter = it.getWriter() as ImmediateFileWriter - assertThat(immediateFileWriter.fileOrchestrator) - .isEqualTo(mockedTargetFileOrchestrator) - } - } - - @Test - fun `M use provide the NoOp processor W consent { NOT_GRANTED }`() { - // WHEN - val processor = testedFactory.resolveProcessor(TrackingConsent.NOT_GRANTED) - - // THEN - assertThat(processor).isInstanceOf(NoOpDataProcessor::class.java) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DefaultConsentAwareDataWriterTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DefaultConsentAwareDataWriterTest.kt deleted file mode 100644 index 1b352b9b57..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DefaultConsentAwareDataWriterTest.kt +++ /dev/null @@ -1,261 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.domain.batching.migrators.BatchedDataMigrator -import com.datadog.android.core.internal.domain.batching.processors.DataProcessor -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class DefaultConsentAwareDataWriterTest { - - @Mock - lateinit var mockedConsentProvider: ConsentProvider - - @Mock - lateinit var mockProcessorFactory: DataProcessorFactory - - @Mock - lateinit var mockedMigratorFactory: MigratorFactory - - @StringForgery - lateinit var fakeEvent: String - - @Mock - lateinit var mockedNoOpMigrator: BatchedDataMigrator - - @Mock - lateinit var mockedMigrator: BatchedDataMigrator - - @Mock - lateinit var mockedProcessor: DataProcessor - - lateinit var testedHandler: DefaultConsentAwareDataWriter - - lateinit var fakeInitialConsent: TrackingConsent - - @BeforeEach - fun `set up`(forge: Forge) { - fakeInitialConsent = forge.aValueFrom(TrackingConsent::class.java) - whenever(mockedConsentProvider.getConsent()) - .thenReturn(fakeInitialConsent) - whenever(mockedMigratorFactory.resolveMigrator(null, fakeInitialConsent)) - .thenReturn( - mockedNoOpMigrator - ) - whenever( - mockProcessorFactory.resolveProcessor( - fakeInitialConsent - ) - ).thenReturn( - mockedProcessor - ) - testedHandler = DefaultConsentAwareDataWriter( - mockedConsentProvider, - mockProcessorFactory, - mockedMigratorFactory - ) - } - - @Test - fun `M register as callback for ConsentProvider W initialising`() { - // THEN - verify(mockedConsentProvider).registerCallback(testedHandler) - } - - @Test - fun `M migrate data W consent changed`(forge: Forge) { - // GIVEN - // should be a value different than current one as this is the contract with the - // TrackingConsentProvider - val fakeNewConsent = forge.aValueFrom( - TrackingConsent::class.java, - listOf(fakeInitialConsent) - ) - whenever( - mockedMigratorFactory.resolveMigrator( - fakeInitialConsent, - fakeNewConsent - ) - ).thenReturn(mockedMigrator) - - // WHEN - testedHandler.onConsentUpdated(fakeInitialConsent, fakeNewConsent) - - // THEN - verify(mockedMigrator).migrateData() - } - - @Test - fun `M process event W requested`() { - // WHEN - testedHandler.write(fakeEvent) - - // THEN - verify(mockedProcessor).consume(fakeEvent) - } - - @Test - fun `M process collection of events W requested`(forge: Forge) { - // GIVEN - val fakeEvents = forge.aList { forge.aString() } - - // WHEN - testedHandler.write(fakeEvents) - - // THEN - verify(mockedProcessor).consume(fakeEvents) - } - - @Test - fun `M process data W requested after updating the consent`(forge: Forge) { - // GIVEN - val mockedNewProcessor: DataProcessor = mock() - val fakeNewConsent = - forge.aValueFrom(TrackingConsent::class.java, listOf(fakeInitialConsent)) - whenever( - mockedMigratorFactory.resolveMigrator( - fakeInitialConsent, - fakeNewConsent - ) - ).thenReturn(mockedMigrator) - whenever( - mockProcessorFactory.resolveProcessor( - fakeNewConsent - ) - ).thenReturn( - mockedNewProcessor - ) - testedHandler.onConsentUpdated(fakeInitialConsent, fakeNewConsent) - - // WHEN - testedHandler.write(fakeEvent) - - // THEN - verify(mockedNewProcessor).consume(fakeEvent) - } - - @Test - fun `M be synchronous W write { event } in concurrent usage`(forge: Forge) { - // GIVEN - val fakeNewConsent = - forge.aValueFrom(TrackingConsent::class.java, listOf(fakeInitialConsent)) - val mockedNewProcessor: DataProcessor = mock() - val countDownLatch = CountDownLatch(2) - whenever( - mockedMigratorFactory.resolveMigrator( - fakeInitialConsent, - fakeNewConsent - ) - ).thenReturn(mockedMigrator) - whenever( - mockProcessorFactory.resolveProcessor( - fakeNewConsent - ) - ).thenReturn( - mockedNewProcessor - ) - - // WHEN - Thread { - testedHandler.onConsentUpdated(fakeInitialConsent, fakeNewConsent) - countDownLatch.countDown() - }.start() - Thread { - // Give time to first thread to acquire the lock - Thread.sleep(1) - testedHandler.write(fakeEvent) - countDownLatch.countDown() - }.start() - - // THEN - countDownLatch.await(1, TimeUnit.SECONDS) - inOrder( - mockedNoOpMigrator, - mockedMigrator, - mockedNewProcessor - ) { - verify(mockedNoOpMigrator).migrateData() - verify(mockedMigrator).migrateData() - verify(mockedNewProcessor).consume(fakeEvent) - } - } - - @Test - fun `M be synchronous W write { events } in concurrent usage`(forge: Forge) { - // GIVEN - val fakeEvents = forge.aList { forge.aString() } - val fakeNewConsent = forge.aValueFrom( - TrackingConsent::class.java, - listOf(fakeInitialConsent) - ) - val mockedNewProcessor: DataProcessor = mock() - val countDownLatch = CountDownLatch(2) - whenever( - mockedMigratorFactory.resolveMigrator( - fakeInitialConsent, - fakeNewConsent - ) - ).thenReturn(mockedMigrator) - whenever( - mockProcessorFactory.resolveProcessor( - fakeNewConsent - ) - ).thenReturn( - mockedNewProcessor - ) - - // WHEN - Thread { - testedHandler.onConsentUpdated(fakeInitialConsent, fakeNewConsent) - countDownLatch.countDown() - }.start() - Thread { - // Give time to first thread to acquire the lock - Thread.sleep(1) - testedHandler.write(fakeEvents) - countDownLatch.countDown() - }.start() - - // THEN - countDownLatch.await(1, TimeUnit.SECONDS) - inOrder( - mockedNoOpMigrator, - mockedMigrator, - mockedNewProcessor - ) { - verify(mockedNoOpMigrator).migrateData() - verify(mockedMigrator).migrateData() - verify(mockedNewProcessor).consume(fakeEvents) - } - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DefaultMigratorFactoryTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DefaultMigratorFactoryTest.kt deleted file mode 100644 index 4d762ff54e..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/DefaultMigratorFactoryTest.kt +++ /dev/null @@ -1,168 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching - -import com.datadog.android.core.internal.domain.batching.migrators.MoveDataMigrator -import com.datadog.android.core.internal.domain.batching.migrators.NoOpBatchedDataMigrator -import com.datadog.android.core.internal.domain.batching.migrators.WipeDataMigrator -import com.datadog.android.privacy.TrackingConsent -import com.nhaarman.mockitokotlin2.mock -import fr.xgouchet.elmyr.Forge -import java.util.concurrent.ExecutorService -import java.util.stream.Stream -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource - -internal class DefaultMigratorFactoryTest { - - lateinit var testedFactory: DefaultMigratorFactory - - @ParameterizedTest - @MethodSource("provideMigratorStatesData") - fun `M generate the right migrator W required`( - previousConsentFlag: TrackingConsent?, - newConsentFlag: TrackingConsent, - pendingFolderPath: String, - acceptedFolderPath: String, - expected: ExpectedMigrator - ) { - - // GIVEN - testedFactory = - DefaultMigratorFactory(pendingFolderPath, acceptedFolderPath, mockExecutorService) - - // WHEN - val migrator = testedFactory.resolveMigrator(previousConsentFlag, newConsentFlag) - - // THEN - when (expected) { - is ExpectedMigrator.WipeMigrator -> { - assertThat(migrator).isInstanceOf(WipeDataMigrator::class.java) - assertThat((migrator as WipeDataMigrator).folderPath).isEqualTo(expected.folderPath) - } - is ExpectedMigrator.MoveMigrator -> { - assertThat(migrator).isInstanceOf(MoveDataMigrator::class.java) - val moveDataMigrator = migrator as MoveDataMigrator - assertThat(moveDataMigrator.pendingFolderPath).isEqualTo(expected.fromFolderPath) - assertThat(moveDataMigrator.approvedFolderPath).isEqualTo(expected.toFolderPath) - } - else -> { - assertThat(migrator).isInstanceOf(NoOpBatchedDataMigrator::class.java) - } - } - } - - companion object { - - var forge = Forge() - val mockExecutorService: ExecutorService = mock() - - @JvmStatic - fun provideMigratorStatesData(): Stream { - val pendingFolderPath = forge.aStringMatching("[a-zA-z]+/[a-zA-z]") - val grantedFolderPath = forge.aStringMatching("[a-zA-z]+/[a-zA-z]") - return Stream.of( - // initial state migrator - Arguments.arguments( - null, - TrackingConsent.PENDING, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.WipeMigrator(pendingFolderPath) - ), - Arguments.arguments( - TrackingConsent.PENDING, - TrackingConsent.PENDING, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.NoOpMigrator - ), - Arguments.arguments( - TrackingConsent.PENDING, - TrackingConsent.NOT_GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.WipeMigrator(pendingFolderPath) - ), - Arguments.arguments( - TrackingConsent.PENDING, - TrackingConsent.GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.MoveMigrator(pendingFolderPath, grantedFolderPath) - ), - // initial state migrator - Arguments.arguments( - null, - TrackingConsent.GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.NoOpMigrator - ), - Arguments.arguments( - TrackingConsent.GRANTED, - TrackingConsent.GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.NoOpMigrator - ), - Arguments.arguments( - TrackingConsent.GRANTED, - TrackingConsent.PENDING, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.WipeMigrator(pendingFolderPath) - ), - Arguments.arguments( - TrackingConsent.GRANTED, - TrackingConsent.NOT_GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.NoOpMigrator - ), - // initial state migrator - Arguments.arguments( - null, - TrackingConsent.NOT_GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.NoOpMigrator - ), - Arguments.arguments( - TrackingConsent.NOT_GRANTED, - TrackingConsent.NOT_GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.NoOpMigrator - ), - Arguments.arguments( - TrackingConsent.NOT_GRANTED, - TrackingConsent.PENDING, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.WipeMigrator(pendingFolderPath) - ), - Arguments.arguments( - TrackingConsent.NOT_GRANTED, - TrackingConsent.GRANTED, - pendingFolderPath, - grantedFolderPath, - ExpectedMigrator.NoOpMigrator - ) - ) - } - } - - internal sealed class ExpectedMigrator { - object NoOpMigrator : ExpectedMigrator() - class WipeMigrator(val folderPath: String) : ExpectedMigrator() - class MoveMigrator(val fromFolderPath: String, val toFolderPath: String) : - ExpectedMigrator() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/migrators/MoveDataMigratorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/migrators/MoveDataMigratorTest.kt deleted file mode 100644 index a9bbd4684e..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/migrators/MoveDataMigratorTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.migrators - -import com.datadog.android.core.internal.data.file.FileHandler -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.ExecutorService -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) -internal class MoveDataMigratorTest { - - @Mock - lateinit var mockedExecutorService: ExecutorService - - @Mock - lateinit var mockedFileHandler: FileHandler - - @StringForgery(regex = "([a-zA-z]+)/([a-zA-z]+)") - lateinit var fakeSourceFolderPath: String - - @StringForgery(regex = "([a-zA-z]+)/([a-zA-z]+)") - lateinit var fakeDestinationFolderPath: String - lateinit var testedMigrator: MoveDataMigrator - - @BeforeEach - fun `set up`() { - testedMigrator = - MoveDataMigrator( - fakeSourceFolderPath, - fakeDestinationFolderPath, - mockedExecutorService, - mockedFileHandler - ) - } - - @Test - fun `M delegate to fileHandler W migrateData`() { - // GIVEN - val argumentCaptor = argumentCaptor() - whenever(mockedFileHandler.moveFiles(any(), any())).thenReturn(true) - - // WHEN - testedMigrator.migrateData() - - // THEN - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedFileHandler).moveFiles( - argThat { this.absolutePath == File(fakeSourceFolderPath).absolutePath }, - argThat { this.absolutePath == File(fakeDestinationFolderPath).absolutePath } - ) - verifyNoMoreInteractions(mockedFileHandler) - } - - @Test - fun `M retry maximum 3 times W migrateData fails`() { - // GIVEN - val argumentCaptor = argumentCaptor() - whenever(mockedFileHandler.moveFiles(any(), any())).thenReturn(false) - - // WHEN - testedMigrator.migrateData() - - // THEN - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedFileHandler, times(3)).moveFiles( - argThat { this.absolutePath == File(fakeSourceFolderPath).absolutePath }, - argThat { this.absolutePath == File(fakeDestinationFolderPath).absolutePath } - ) - } - - @Test - fun `M retry W migrateData fails`() { - // GIVEN - val argumentCaptor = argumentCaptor() - whenever(mockedFileHandler.moveFiles(any(), any())) - .thenReturn(false) - .thenReturn(true) - - // WHEN - testedMigrator.migrateData() - - // THEN - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedFileHandler, times(2)).moveFiles( - argThat { this.absolutePath == File(fakeSourceFolderPath).absolutePath }, - argThat { this.absolutePath == File(fakeDestinationFolderPath).absolutePath } - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/migrators/WipeDataMigratorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/migrators/WipeDataMigratorTest.kt deleted file mode 100644 index a786135441..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/migrators/WipeDataMigratorTest.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.migrators - -import com.datadog.android.core.internal.data.file.FileHandler -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.ExecutorService -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) -internal class WipeDataMigratorTest { - - @Mock - lateinit var mockedExecutorService: ExecutorService - - @Mock - lateinit var mockedFileHandler: FileHandler - - @StringForgery(regex = "([a-zA-z]+)/([a-zA-z]+)") - lateinit var fakeFolderPath: String - lateinit var testedMigrator: WipeDataMigrator - - @BeforeEach - fun `set up`() { - testedMigrator = WipeDataMigrator(fakeFolderPath, mockedExecutorService, mockedFileHandler) - } - - @Test - fun `M delegate to fileHandler W migrateData`() { - // GIVEN - whenever(mockedFileHandler.deleteFileOrDirectory(any())).thenReturn(true) - val argumentCaptor = argumentCaptor() - - // WHEN - testedMigrator.migrateData() - - // THEN - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedFileHandler).deleteFileOrDirectory( - argThat { - this.absolutePath == File(fakeFolderPath).absolutePath - } - ) - } - - @Test - fun `M retry maximum 3 times W migrateData fails`() { - // GIVEN - val argumentCaptor = argumentCaptor() - whenever(mockedFileHandler.deleteFileOrDirectory(any())).thenReturn(false) - - // WHEN - testedMigrator.migrateData() - - // THEN - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedFileHandler, times(3)).deleteFileOrDirectory( - argThat { - this.absolutePath == File(fakeFolderPath).absolutePath - } - ) - } - - @Test - fun `M retry W migrateData fails`() { - // GIVEN - val argumentCaptor = argumentCaptor() - whenever(mockedFileHandler.deleteFileOrDirectory(any())) - .thenReturn(false) - .thenReturn(true) - - // WHEN - testedMigrator.migrateData() - - // THEN - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedFileHandler, times(2)).deleteFileOrDirectory( - argThat { - this.absolutePath == File(fakeFolderPath).absolutePath - } - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/processors/DefaultDataProcessorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/processors/DefaultDataProcessorTest.kt deleted file mode 100644 index b29bacb432..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/domain/batching/processors/DefaultDataProcessorTest.kt +++ /dev/null @@ -1,74 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.domain.batching.processors - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.ExecutorService -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) -internal class DefaultDataProcessorTest { - - lateinit var testedDataProcessor: DataProcessor - - @Mock - lateinit var mockedExecutorService: ExecutorService - - @Mock - lateinit var mockedWriter: Writer - - @BeforeEach - fun `set up`() { - testedDataProcessor = DefaultDataProcessor(mockedExecutorService, mockedWriter) - } - - @Test - fun `M delegate to writer W a new event is consumed`(@StringForgery fakeEvent: String) { - // WHEN - testedDataProcessor.consume(fakeEvent) - - // THEN - val argumentCaptor = argumentCaptor() - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedWriter).write(fakeEvent) - } - - @Test - fun `M delegate to writer W a list of events is consumed`(forge: Forge) { - // GIVEN - val fakeEvents = forge.aList { forge.aString() } - - // WHEN - testedDataProcessor.consume(fakeEvents) - - // THEN - val argumentCaptor = argumentCaptor() - verify(mockedExecutorService).submit(argumentCaptor.capture()) - argumentCaptor.firstValue.run() - verify(mockedWriter).write(fakeEvents) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallbackTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallbackTest.kt deleted file mode 100644 index d94a6ad989..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/lifecycle/ProcessLifecycleCallbackTest.kt +++ /dev/null @@ -1,138 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.lifecycle - -import android.content.Context -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.impl.WorkManagerImpl -import com.datadog.android.core.internal.data.upload.UploadWorker -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.utils.TAG_DATADOG_UPLOAD -import com.datadog.android.core.internal.utils.UPLOAD_WORKER_NAME -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.setFieldValue -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import java.lang.ref.WeakReference -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class ProcessLifecycleCallbackTest { - - lateinit var testedCallback: ProcessLifecycleCallback - - lateinit var mockContext: Context - - @Mock - lateinit var mockWorkManager: WorkManagerImpl - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @BeforeEach - fun `set up`() { - mockContext = mockContext() - testedCallback = ProcessLifecycleCallback(mockNetworkInfoProvider, mockContext) - } - - @AfterEach - fun `tear down`() { - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", null) - } - - @Test - fun `when process stopped and network is disconnected will schedule an upload worker`() { - // Given - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) - .thenReturn( - NetworkInfo( - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED - ) - ) - - // When - testedCallback.onStopped() - - // Then - verify(mockWorkManager).enqueueUniqueWork( - eq(UPLOAD_WORKER_NAME), - eq(ExistingWorkPolicy.REPLACE), - argThat { - this.workSpec.workerClassName == UploadWorker::class.java.canonicalName && - this.tags.contains(TAG_DATADOG_UPLOAD) - } - ) - } - - @Test - fun `when process stopped and work manager is not present will not throw exception`() { - // Given - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) - .thenReturn( - NetworkInfo( - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED - ) - ) - - // When - testedCallback.onStopped() - } - - @Test - fun `when process stopped and network is connected will do nothing`() { - // Given - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) - .thenReturn( - NetworkInfo( - NetworkInfo.Connectivity.NETWORK_WIFI - ) - ) - - // When - testedCallback.onStopped() - - // Then - verifyZeroInteractions(mockWorkManager) - } - - @Test - fun `when process stopped and context ref is null will do nothing`() { - testedCallback.setFieldValue("contextWeakRef", WeakReference(null)) - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) - .thenReturn( - NetworkInfo( - NetworkInfo.Connectivity.NETWORK_WIFI - ) - ) - - // When - testedCallback.onStopped() - - // Then - verifyZeroInteractions(mockWorkManager) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/DataOkHttpUploaderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/DataOkHttpUploaderTest.kt deleted file mode 100644 index c2867ff234..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/DataOkHttpUploaderTest.kt +++ /dev/null @@ -1,374 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import android.os.Build -import com.datadog.android.BuildConfig -import com.datadog.tools.unit.setStaticValue -import fr.xgouchet.elmyr.Forge -import java.util.concurrent.TimeUnit -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -internal abstract class DataOkHttpUploaderTest { - - lateinit var mockWebServer: MockWebServer - - lateinit var testedUploader: T - - lateinit var fakeEndpoint: String - lateinit var fakeToken: String - lateinit var fakeUserAgent: String - - @BeforeEach - open fun `set up`(forge: Forge) { - - Build.VERSION::class.java.setStaticValue("RELEASE", forge.anAlphaNumericalString()) - Build::class.java.setStaticValue("MODEL", forge.anAlphabeticalString()) - Build::class.java.setStaticValue("ID", forge.anAlphabeticalString()) - - mockWebServer = MockWebServer() - mockWebServer.start() - fakeEndpoint = mockWebServer.url("http://github.com/").toString().removeSuffix("/") - fakeToken = forge.anHexadecimalString() - fakeUserAgent = if (forge.aBool()) forge.anAlphaNumericalString() else "" - println("fakeUserAgent:$fakeUserAgent") - println("RELEASE:${Build.VERSION.RELEASE}") - println("MODEL:${Build.MODEL}") - println("ID:${Build.ID}") - System.setProperty("http.agent", fakeUserAgent) - testedUploader = uploader() - } - - abstract fun uploader(): T - - abstract fun urlFormat(): String - - abstract fun expectedPathRegex(): String - - @AfterEach - open fun `tear down`() { - mockWebServer.shutdown() - - Build.VERSION::class.java.setStaticValue("RELEASE", null) - Build::class.java.setStaticValue("MODEL", null) - Build::class.java.setStaticValue("ID", null) - } - - @Test - fun `uploads data 100-Continue (timeout)`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(100)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 1xx-Informational`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(101, 200))) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.UNKNOWN_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 200-OK`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(200)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.SUCCESS) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 204-NO CONTENT`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(204)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 205-RESET`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(205)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 2xx-Success`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(206, 299))) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.SUCCESS) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 3xx-Redirection`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(300, 399))) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.HTTP_REDIRECTION) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 400-BadRequest`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(400)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.HTTP_CLIENT_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 401-Unauthorized`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(401)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.HTTP_CLIENT_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 403-Forbidden`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(403)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.INVALID_TOKEN_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 404-NotFound`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(404)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.HTTP_CLIENT_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 407-Proxy`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(407)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 408-Timeout`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(408)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 4xx-ClientError`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(409, 499))) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.HTTP_CLIENT_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 500-InternalServerError`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(500)) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.HTTP_SERVER_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data 5xx-ServerError`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(500, 599))) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.HTTP_SERVER_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads data xxx-InvalidError`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(600, 1000))) - - val status = testedUploader.upload(data) - - assertThat(status).isEqualTo(UploadStatus.UNKNOWN_ERROR) - assertRequestIsValid(mockWebServer.takeRequest(), anHexadecimalString) - } - - @Test - fun `uploads with IOException (timeout)`(forge: Forge) { - val data = forge.anHexadecimalString().toByteArray(Charsets.UTF_8) - mockWebServer.enqueue( - MockResponse() - .throttleBody( - THROTTLE_RATE, - THROTTLE_PERIOD_MS, - TimeUnit.MILLISECONDS - ) - .setBody( - "{ 'success': 'ok', 'message': 'Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, " + - "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' }" - ) - ) - - val status = testedUploader.upload(data) - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - } - - @Test - fun `uploads with IOException (protocol)`(forge: Forge) { - val data = forge.anHexadecimalString().toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(0, 100))) - - val status = testedUploader.upload(data) - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - } - - @Test - fun `uploads with IOException (protocol 2)`(forge: Forge) { - val data = forge.anHexadecimalString().toByteArray(Charsets.UTF_8) - mockWebServer.enqueue(forgeMockResponse(forge.anInt(1000))) - - val status = testedUploader.upload(data) - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - } - - @Test - fun `uploads with IOException (no server)`(forge: Forge) { - val anHexadecimalString = forge.anHexadecimalString() - val data = anHexadecimalString.toByteArray(Charsets.UTF_8) - mockWebServer.shutdown() - - val status = testedUploader.upload(data) - assertThat(status).isEqualTo(UploadStatus.NETWORK_ERROR) - } - - @Test - fun `uploads with updated endpoint`(forge: Forge) { - val data = forge.anHexadecimalString().toByteArray(Charsets.UTF_8) - mockWebServer.shutdown() - val mockWebServer2 = MockWebServer() - mockWebServer2.start(forge.anInt(2000, 8000)) - mockWebServer2.enqueue(forgeMockResponse(200)) - fakeEndpoint = mockWebServer2.url("http://github.com/").toString().removeSuffix("/") - - testedUploader.setEndpoint(fakeEndpoint) - val status = testedUploader.upload(data) - assertThat(status).isEqualTo(UploadStatus.SUCCESS) - } - - // region Internal - - private fun assertRequestIsValid( - request: RecordedRequest, - data: String - ) { - assertRequestHasExpectedHeaders(request) - - assertThat(request.path).matches(expectedPathRegex()) - assertThat(request.body.readUtf8()) - .isEqualTo(data) - } - - private fun assertRequestHasExpectedHeaders(request: RecordedRequest) { - assertThat(request.getHeader("Content-Type")) - .isEqualTo(testedUploader.contentType) - val expectedUserAgent = if (fakeUserAgent.isBlank()) { - "Datadog/${BuildConfig.VERSION_NAME} " + - "(Linux; U; Android ${Build.VERSION.RELEASE}; " + - "${Build.MODEL} Build/${Build.ID})" - } else { - fakeUserAgent - } - assertThat(request.getHeader("User-Agent")) - .isEqualTo(expectedUserAgent) - } - - private fun forgeMockResponse(code: Int): MockResponse { - return MockResponse() - .setResponseCode(code) - .setBody("{}") - } - - // endregion - - companion object { - const val TIMEOUT_TEST_MS = 250L - const val THROTTLE_RATE = 8L - const val THROTTLE_PERIOD_MS = TIMEOUT_TEST_MS * 2 - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/FirstPartyHostDetectorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/FirstPartyHostDetectorTest.kt deleted file mode 100644 index 505430edeb..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/FirstPartyHostDetectorTest.kt +++ /dev/null @@ -1,224 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import okhttp3.HttpUrl -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class FirstPartyHostDetectorTest { - - lateinit var testedDetector: FirstPartyHostDetector - - @StringForgery(regex = HOST_REGEX) - lateinit var fakeHosts: List - - @BeforeEach - fun `set up`() { - testedDetector = FirstPartyHostDetector(fakeHosts) - } - - @Test - fun `𝕄 return false 𝕎 isFirstParty(HttpUrl) {unknown host}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - var host = forge.aStringMatching(HOST_REGEX) - while (host in fakeHosts) { - host = forge.aStringMatching(HOST_REGEX) - } - val url = HttpUrl.get("$scheme://$host$path") - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isFalse() - } - - @Test - fun `𝕄 return true 𝕎 isFirstParty(HttpUrl) {exact first party host}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - val host = forge.anElementFrom(fakeHosts) - val url = HttpUrl.get("$scheme://$host$path") - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isTrue() - } - - @Test - fun `𝕄 return true 𝕎 isFirstParty(HttpUrl) {valid host subdomain}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") subdomain: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - val host = forge.anElementFrom(fakeHosts) - val url = HttpUrl.get("$scheme://$subdomain.$host$path") - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isTrue() - } - - @Test - fun `𝕄 return false 𝕎 isFirstParty(HttpUrl) {unknown host postfixed with valid host}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") prefix: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - val host = forge.anElementFrom(fakeHosts) - val url = HttpUrl.get("$scheme://$prefix$host$path") - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isFalse() - } - - @Test - fun `𝕄 return false 𝕎 isFirstParty(String) {unknown host}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - var host = forge.aStringMatching(HOST_REGEX) - while (host in fakeHosts) { - host = forge.aStringMatching(HOST_REGEX) - } - val url = "$scheme://$host$path" - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isFalse() - } - - @Test - fun `𝕄 return true 𝕎 isFirstParty(String) {exact first party host}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - val host = forge.anElementFrom(fakeHosts) - val url = "$scheme://$host$path" - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isTrue() - } - - @Test - fun `𝕄 return true 𝕎 isFirstParty(String) {valid host subdomain}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") subdomain: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - val host = forge.anElementFrom(fakeHosts) - val url = "$scheme://$subdomain.$host$path" - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isTrue() - } - - @Test - fun `𝕄 return false 𝕎 isFirstParty(String) {unknown host postfixed with valid host}`( - @StringForgery(regex = "http(s?)") scheme: String, - @StringForgery(regex = "[a-zA-Z0-9_~-]{1,9}") prefix: String, - @StringForgery(regex = "(/[a-zA-Z0-9_~\\.-]{1,9}){1,4}") path: String, - forge: Forge - ) { - // Given - val host = forge.anElementFrom(fakeHosts) - val url = "$scheme://$prefix$host$path" - - // When - val result = testedDetector.isFirstPartyUrl(url) - - // Then - assertThat(result).isFalse() - } - - @Test - fun `𝕄 return false 𝕎 isFirstParty(String) {invalid url}`( - @StringForgery notAUrl: String, - forge: Forge - ) { - // When - val result = testedDetector.isFirstPartyUrl(notAUrl) - - // Then - assertThat(result).isFalse() - } - - @Test - fun `𝕄 return true 𝕎 isEmpty() {empty host list}`() { - // Given - val detector = FirstPartyHostDetector(emptyList()) - - // When - val result = detector.isEmpty() - - // Then - assertThat(result).isTrue() - } - - @Test - fun `𝕄 return false 𝕎 isEmpty() {non empty host list}`() { - // When - val result = testedDetector.isEmpty() - - // Then - assertThat(result).isFalse() - } - - companion object { - private const val HOST_REGEX = "([a-z][a-z0-9_~-]{3,9}\\.){1,4}[a-z][a-z0-9]{2,3}" - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/GzipRequestInterceptorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/GzipRequestInterceptorTest.kt deleted file mode 100644 index 88f113fd9a..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/GzipRequestInterceptorTest.kt +++ /dev/null @@ -1,150 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.ByteArrayOutputStream -import okhttp3.Interceptor -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.Response -import okio.Buffer -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class GzipRequestInterceptorTest { - lateinit var testedInterceptor: Interceptor - - @Mock - lateinit var mockChain: Interceptor.Chain - - lateinit var fakeRequest: Request - lateinit var fakeResponse: Response - lateinit var fakeBody: String - - @BeforeEach - fun `set up`(forge: Forge) { - val fakeUrl = forge.aStringMatching("http://[a-z0-9_]{8}\\.[a-z]{3}") - fakeBody = forge.anAlphabeticalString() - fakeRequest = Request.Builder() - .url(http://github.com/fakeUrl) - .post(RequestBody.create(null, fakeBody.toByteArray())) - .build() - testedInterceptor = GzipRequestInterceptor() - } - - @Test - fun `compress body when no encoding is used`() { - fakeResponse = forgeResponse() - stubChain() - - val response = testedInterceptor.intercept(mockChain) - - argumentCaptor { - verify(mockChain).proceed(capture()) - val buffer = Buffer() - val stream = ByteArrayOutputStream() - lastValue.body()!!.writeTo(buffer) - buffer.copyTo(stream) - - assertThat(stream.toString()) - .isNotEqualTo(fakeBody) - - assertThat(lastValue.header("Content-Encoding")) - .isEqualTo("gzip") - } - assertThat(response) - .isSameAs(fakeResponse) - } - - @Test - fun `ignores body when encoding is set`() { - fakeRequest = fakeRequest.newBuilder() - .header("Content-Encoding", "identity") - .build() - fakeResponse = forgeResponse() - stubChain() - - val response = testedInterceptor.intercept(mockChain) - - argumentCaptor { - verify(mockChain).proceed(capture()) - val buffer = Buffer() - val stream = ByteArrayOutputStream() - lastValue.body()!!.writeTo(buffer) - buffer.copyTo(stream) - - assertThat(stream.toString()) - .isEqualTo(fakeBody) - assertThat(lastValue.header("Content-Encoding")) - .isEqualTo("identity") - } - assertThat(response) - .isSameAs(fakeResponse) - } - - @Test - fun `ignores body when body is null`() { - fakeRequest = fakeRequest.newBuilder() - .get() - .build() - fakeResponse = forgeResponse() - stubChain() - - val response = testedInterceptor.intercept(mockChain) - - argumentCaptor { - verify(mockChain).proceed(capture()) - assertThat(lastValue.body()) - .isNull() - assertThat(lastValue.header("Content-Encoding")) - .isNull() - } - assertThat(response) - .isSameAs(fakeResponse) - } - - // region Internal - - private fun forgeResponse(): Response { - val builder = Response.Builder() - .request(fakeRequest) - .protocol(Protocol.HTTP_2) - .code(200) - .message("{}") - return builder.build() - } - - private fun stubChain() { - whenever(mockChain.request()) doReturn fakeRequest - whenever(mockChain.proceed(any())) doReturn fakeResponse - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/RequestUniqueIdentifierTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/RequestUniqueIdentifierTest.kt deleted file mode 100644 index 3f70a22b7e..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/RequestUniqueIdentifierTest.kt +++ /dev/null @@ -1,176 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import okhttp3.MediaType -import okhttp3.Request -import okhttp3.RequestBody -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RequestUniqueIdentifierTest { - - @RegexForgery("http(s?)://[a-z]+\\.com/\\w+") - private lateinit var fakeUrl: String - - @RegexForgery("x-[a-z]+/[a-z]+") - private lateinit var fakeContentType: String - - @StringForgery - private lateinit var fakeBody: String - - @Test - fun `identify GET request`() { - val request = Request.Builder() - .get().url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - assertThat(id).isEqualTo("GET•$fakeUrl") - } - - @Test - fun `identify POST request`() { - val body = RequestBody.create(null, fakeBody) - val request = Request.Builder() - .post(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("POST•$fakeUrl•$contentLength•null") - } - - @Test - fun `identify POST request with content type`() { - val body = RequestBody.create(MediaType.parse(fakeContentType), fakeBody) - val request = Request.Builder() - .post(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("POST•$fakeUrl•$contentLength•$fakeContentType; charset=utf-8") - } - - @Test - fun `identify PUT request`() { - val body = RequestBody.create(null, fakeBody) - val request = Request.Builder() - .put(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("PUT•$fakeUrl•$contentLength•null") - } - - @Test - fun `identify PUT request with content type`() { - val body = RequestBody.create(MediaType.parse(fakeContentType), fakeBody) - val request = Request.Builder() - .put(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("PUT•$fakeUrl•$contentLength•$fakeContentType; charset=utf-8") - } - - @Test - fun `identify PATCH request`() { - val body = RequestBody.create(null, fakeBody) - val request = Request.Builder() - .patch(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("PATCH•$fakeUrl•$contentLength•null") - } - - @Test - fun `identify PATCH request with content type`() { - val body = RequestBody.create(MediaType.parse(fakeContentType), fakeBody) - val request = Request.Builder() - .patch(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("PATCH•$fakeUrl•$contentLength•$fakeContentType; charset=utf-8") - } - - @Test - fun `identify DELETE request`() { - val request = Request.Builder() - .delete().url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - assertThat(id) - .isEqualTo("DELETE•$fakeUrl") - } - - @Test - fun `identify DELETE request with body`() { - val body = RequestBody.create(null, fakeBody) - val request = Request.Builder() - .delete(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("DELETE•$fakeUrl•$contentLength•null") - } - - @Test - fun `identify DELETE request with content type`() { - val body = RequestBody.create(MediaType.parse(fakeContentType), fakeBody) - val request = Request.Builder() - .delete(body).url(http://github.com/fakeUrl) - .build() - - val id = identifyRequest(request) - - val contentLength = fakeBody.length - assertThat(id) - .isEqualTo("DELETE•$fakeUrl•$contentLength•$fakeContentType; charset=utf-8") - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/UploadStatusTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/UploadStatusTest.kt deleted file mode 100644 index c3b143a6f1..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/UploadStatusTest.kt +++ /dev/null @@ -1,129 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net - -import android.util.Log -import com.datadog.android.Datadog -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockDevLogHandler -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import kotlin.math.min -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class UploadStatusTest { - - lateinit var fakeContext: String - lateinit var mockDevLogHandler: LogHandler - - @BeforeEach - fun `set up`(forge: Forge) { - mockDevLogHandler = mockDevLogHandler() - fakeContext = forge.anAlphabeticalString() - Datadog.setVerbosity(Log.VERBOSE) - } - - @Test - fun `logStatus SUCCESS`(@IntForgery(min = 0) byteSize: Int) { - UploadStatus.SUCCESS.logStatus(fakeContext, byteSize) - - verify(mockDevLogHandler) - .handleLog( - Log.VERBOSE, - "Batch [$byteSize bytes] sent successfully ($fakeContext)." - ) - } - - @Test - fun `logStatus NETWORK_ERROR`(@IntForgery(min = 0) byteSize: Int) { - UploadStatus.NETWORK_ERROR.logStatus(fakeContext, byteSize) - - verify(mockDevLogHandler) - .handleLog( - Log.ERROR, - "Unable to send batch [$byteSize bytes] ($fakeContext) " + - "because of a network error; we will retry later." - ) - } - - @Test - fun `logStatus INVALID_TOKEN_ERROR`(@IntForgery(min = 0) byteSize: Int) { - UploadStatus.INVALID_TOKEN_ERROR.logStatus(fakeContext, byteSize) - - verify(mockDevLogHandler) - .handleLog( - Log.ERROR, - "Unable to send batch [$byteSize bytes] ($fakeContext) " + - "because your token is invalid. Make sure that the provided token still exists." - ) - } - - @Test - fun `logStatus HTTP_REDIRECTION`(@IntForgery(min = 0) byteSize: Int) { - UploadStatus.HTTP_REDIRECTION.logStatus(fakeContext, byteSize) - - verify(mockDevLogHandler) - .handleLog( - Log.WARN, - "Unable to send batch [$byteSize bytes] ($fakeContext) " + - "because of a network error; we will retry later." - ) - } - - @Test - fun `logStatus HTTP_CLIENT_ERROR`(@IntForgery(min = 0) byteSize: Int) { - UploadStatus.HTTP_CLIENT_ERROR.logStatus(fakeContext, byteSize) - - verify(mockDevLogHandler) - .handleLog( - Log.ERROR, - "Unable to send batch [$byteSize bytes] ($fakeContext) " + - "because of a processing error (possibly because of invalid data); " + - "the batch was dropped." - ) - } - - @Test - fun `logStatus HTTP_SERVER_ERROR`(@IntForgery(min = 0) byteSize: Int) { - UploadStatus.HTTP_SERVER_ERROR.logStatus(fakeContext, byteSize) - - verify(mockDevLogHandler) - .handleLog( - Log.ERROR, - "Unable to send batch [$byteSize bytes] ($fakeContext) " + - "because of a server processing error; we will retry later." - ) - } - - @Test - fun `logStatus UNKNOWN_ERROR`(@IntForgery(min = 0) byteSize: Int) { - UploadStatus.UNKNOWN_ERROR.logStatus(fakeContext, byteSize) - - verify(mockDevLogHandler) - .handleLog( - Log.ERROR, - "Unable to send batch [$byteSize bytes] ($fakeContext) " + - "because of an unknown error; we will retry later." - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt deleted file mode 100644 index ac0a00ddc2..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt +++ /dev/null @@ -1,470 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net.info - -import android.content.Context -import android.content.Intent -import android.net.ConnectivityManager -import android.net.NetworkInfo as AndroidNetworkInfo -import android.os.Build -import android.telephony.TelephonyManager -import com.datadog.android.log.assertj.NetworkInfoAssert.Companion.assertThat -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -@Suppress("DEPRECATION") -internal class BroadcastReceiverNetworkInfoProviderTest { - - lateinit var testedProvider: BroadcastReceiverNetworkInfoProvider - - @Mock - lateinit var mockContext: Context - - @Mock - lateinit var mockConnectivityManager: ConnectivityManager - - @Mock - lateinit var mockTelephonyManager: TelephonyManager - - @Mock - lateinit var mockNetworkInfo: AndroidNetworkInfo - - @Mock - lateinit var mockIntent: Intent - - @BeforeEach - fun `set up`() { - whenever(mockContext.getSystemService(Context.CONNECTIVITY_SERVICE)) - .doReturn(mockConnectivityManager) - whenever(mockContext.getSystemService(Context.TELEPHONY_SERVICE)) - .doReturn(mockTelephonyManager) - whenever(mockConnectivityManager.activeNetworkInfo) doReturn mockNetworkInfo - - testedProvider = BroadcastReceiverNetworkInfoProvider() - } - - @Test - fun `initial state is not connected`() { - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - @Test - fun `it will do nothing if unregister is called before register`() { - // When - testedProvider.unregister(mockContext) - - // Then - verifyZeroInteractions(mockContext) - } - - @Test - fun `it will unregister the receiver only once`() { - // Given - val countDownLatch = CountDownLatch(2) - testedProvider.register(mockContext) - - // When - Thread { - testedProvider.unregister(mockContext) - countDownLatch.countDown() - }.start() - Thread { - testedProvider.unregister(mockContext) - countDownLatch.countDown() - }.start() - - // Then - countDownLatch.await(3, TimeUnit.SECONDS) - verify(mockContext).unregisterReceiver(testedProvider) - } - - @Test - fun `read network info on register`() { - stubNetworkInfo(ConnectivityManager.TYPE_WIFI, -1) - - testedProvider.register(mockContext) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - @Test - fun `not connected (null)`() { - whenever(mockConnectivityManager.activeNetworkInfo) doReturn null - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - @Test - fun `not connected (connected but no internet)`() { - stubNetworkInfo(-1 /* @hide ConnectivityManager.TYPE_NONE*/, -1) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - @Test - fun `connected to wifi`() { - stubNetworkInfo(ConnectivityManager.TYPE_WIFI, -1) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - @Test - fun `connected to ethernet`() { - stubNetworkInfo(ConnectivityManager.TYPE_ETHERNET, -1) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_ETHERNET) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - @Test - fun `connected to mobile 2G`(forge: Forge) { - val subtype = forge.anElementFrom(known2GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_2G) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.P) - fun `connected to mobile 2G API 28+`(forge: Forge) { - val subtype = forge.anElementFrom(known2GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_2G) - .hasCarrierName(carrierName) - .hasCarrierId(carrierId) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - fun `connected to mobile 3G`(forge: Forge) { - val subtype = forge.anElementFrom(known3GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_3G) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.P) - fun `connected to mobile 3G API 28+`(forge: Forge) { - val subtype = forge.anElementFrom(known3GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_3G) - .hasCarrierName(carrierName) - .hasCarrierId(carrierId) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - fun `connected to mobile 4G`(forge: Forge) { - val subtype = forge.anElementFrom(known4GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_4G) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.P) - fun `connected to mobile 4G API 28+`(forge: Forge) { - val subtype = forge.anElementFrom(known4GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_4G) - .hasCarrierName(carrierName) - .hasCarrierId(carrierId) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - fun `connected to mobile 5G`(forge: Forge) { - val subtype = forge.anElementFrom(known5GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_5G) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.P) - fun `connected to mobile 5G API 28+`(forge: Forge) { - val subtype = forge.anElementFrom(known5GSubtypes) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_5G) - .hasCarrierName(carrierName) - .hasCarrierId(carrierId) - .hasCellularTechnology(mobileSubtypeNames[subtype]) - } - - @Test - fun `connected to mobile unknown`(forge: Forge) { - val subtype = forge.anInt(min = 32) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.P) - fun `connected to mobile unknown API 28+`(forge: Forge) { - val subtype = forge.anInt(min = 32) - val carrierName = forge.anAlphabeticalString() - val carrierId = forge.aPositiveInt(strict = true) - stubNetworkInfo(forge.anElementFrom(knownMobileTypes), subtype) - stubTelephonyManager(carrierName, carrierId) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER) - .hasCarrierName(carrierName) - .hasCarrierId(carrierId) - .hasCellularTechnology(null) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.P) - fun `connected to mobile unknown carrier`(forge: Forge) { - stubNetworkInfo( - forge.anElementFrom(knownMobileTypes), - TelephonyManager.NETWORK_TYPE_UNKNOWN - ) - stubTelephonyManager(null, 0) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER) - .hasCarrierName("Unknown Carrier Name") - .hasCarrierId(0) - .hasCellularTechnology(null) - } - - @Test - fun `connected to unknown network`(forge: Forge) { - stubNetworkInfo(forge.anInt(min = 6), -1) - testedProvider.onReceive(mockContext, mockIntent) - - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasCellularTechnology(null) - } - - // region Internal - - private fun stubTelephonyManager(carrierName: String?, carrierId: Int) { - whenever(mockTelephonyManager.simCarrierIdName) doReturn carrierName - whenever(mockTelephonyManager.simCarrierId) doReturn carrierId - } - - private fun stubNetworkInfo(networkType: Int, networkSubtype: Int) { - if (networkType < 0) { - whenever(mockNetworkInfo.isConnected) doReturn false - whenever(mockNetworkInfo.type) doReturn -1 - } else { - whenever(mockNetworkInfo.isConnected) doReturn true - whenever(mockNetworkInfo.type) doReturn networkType - } - whenever(mockNetworkInfo.subtype) doReturn networkSubtype - } - - // endregion - - companion object { - - private val mobileSubtypeNames = arrayOf( - "unknown", "GPRS", "Edge", "UMTS", "CDMA", "CDMAEVDORev0", "CDMAEVDORevA", "CDMA1x", - "HSDPA", "HSUPA", "HSPA", "iDen", "CDMAEVDORevB", "LTE", "eHRPD", "HSPA+", "GSM", - "TD_SCDMA", "IWLAN", "LTE_CA", "New Radio" - ) - - internal val knownMobileTypes = listOf( - ConnectivityManager.TYPE_MOBILE, - ConnectivityManager.TYPE_MOBILE_DUN, - ConnectivityManager.TYPE_MOBILE_HIPRI, - ConnectivityManager.TYPE_MOBILE_MMS, - ConnectivityManager.TYPE_MOBILE_SUPL - ) - - internal val known2GSubtypes = listOf( - TelephonyManager.NETWORK_TYPE_GPRS, - TelephonyManager.NETWORK_TYPE_EDGE, - TelephonyManager.NETWORK_TYPE_CDMA, - TelephonyManager.NETWORK_TYPE_1xRTT, - TelephonyManager.NETWORK_TYPE_IDEN, - TelephonyManager.NETWORK_TYPE_GSM - ) - - internal val known3GSubtypes = listOf( - TelephonyManager.NETWORK_TYPE_UMTS, - TelephonyManager.NETWORK_TYPE_EVDO_0, - TelephonyManager.NETWORK_TYPE_EVDO_A, - TelephonyManager.NETWORK_TYPE_HSDPA, - TelephonyManager.NETWORK_TYPE_HSUPA, - TelephonyManager.NETWORK_TYPE_HSPA, - TelephonyManager.NETWORK_TYPE_EVDO_B, - TelephonyManager.NETWORK_TYPE_EHRPD, - TelephonyManager.NETWORK_TYPE_HSPAP, - TelephonyManager.NETWORK_TYPE_TD_SCDMA - ) - internal val known4GSubtypes = listOf( - TelephonyManager.NETWORK_TYPE_LTE, - TelephonyManager.NETWORK_TYPE_IWLAN, - 19 // @Hide TelephonyManager.NETWORK_TYPE_LTE_CA, - ) - internal val known5GSubtypes = listOf( - TelephonyManager.NETWORK_TYPE_NR - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt deleted file mode 100644 index fcb2d04cc4..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt +++ /dev/null @@ -1,423 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.net.info - -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities -import android.os.Build -import android.util.Log -import com.datadog.android.log.assertj.NetworkInfoAssert.Companion.assertThat -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockDevLogHandler -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class CallbackNetworkInfoProviderTest { - - lateinit var testedProvider: CallbackNetworkInfoProvider - - @Mock - lateinit var mockNetwork: Network - @Mock - lateinit var mockCapabilities: NetworkCapabilities - @Mock - lateinit var mockDevLogHandler: LogHandler - - @BeforeEach - fun `set up`() { - mockDevLogHandler = mockDevLogHandler() - whenever(mockCapabilities.hasTransport(any())) doReturn false - - testedProvider = - CallbackNetworkInfoProvider() - } - - @Test - fun `initial state is not connected`() { - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(-1) - .hasDownSpeed(-1) - .hasStrength(Int.MIN_VALUE) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.Q) - fun `connected to wifi`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int, - @IntForgery(min = -90, max = -40) strength: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) doReturn true - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - whenever(mockCapabilities.signalStrength) doReturn strength - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - .hasStrength(strength) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `connected to wifi (no strength)`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) doReturn true - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - .hasStrength(Int.MIN_VALUE) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.Q) - fun `connected to wifi aware`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int, - @IntForgery(min = -90, max = -40) strength: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) - .doReturn(true) - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - whenever(mockCapabilities.signalStrength) doReturn strength - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - .hasStrength(strength) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `connected to wifi aware (no strength)`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) - .doReturn(true) - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - .hasStrength(Int.MIN_VALUE) - } - - @Test - fun `connected to cellular`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) - .doReturn(true) - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_CELLULAR) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - } - - @Test - fun `connected to ethernet`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) - .doReturn(true) - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_ETHERNET) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - } - - @Test - fun `connected to VPN`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) doReturn true - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - } - - @Test - fun `connected to LoWPAN`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN)) - .doReturn(true) - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - } - - @Test - fun `network lost`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - whenever(mockCapabilities.hasTransport(any())) doReturn true - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) - testedProvider.onLost(mockNetwork) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(-1) - .hasDownSpeed(-1) - } - - @Test - fun `M register callback W register()`() { - val context = mock() - val manager = mock() - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - - testedProvider.register(context) - - verify(manager).registerDefaultNetworkCallback(testedProvider) - } - - @Test - fun `M get current network state W register()`( - @IntForgery(min = 1) upSpeed: Int, - @IntForgery(min = 1) downSpeed: Int - ) { - val context = mock() - val manager = mock() - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - whenever(manager.activeNetwork) doReturn mockNetwork - whenever(manager.getNetworkCapabilities(mockNetwork)) doReturn mockCapabilities - whenever(mockCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) doReturn true - whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed - whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed - - testedProvider.register(context) - val networkInfo = testedProvider.getLatestNetworkInfo() - - verify(manager).registerDefaultNetworkCallback(testedProvider) - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_WIFI) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(upSpeed) - .hasDownSpeed(downSpeed) - .hasStrength(Int.MIN_VALUE) - } - - @Test - fun `M register callback safely W register() with SecurityException`( - @StringForgery message: String - ) { - // RUMM-852 in some cases the device throws a SecurityException on register - val context = mock() - val manager = mock() - val exception = SecurityException(message) - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception - - testedProvider.register(context) - - verify(manager).registerDefaultNetworkCallback(testedProvider) - } - - @Test - fun `M warn developers W register() with SecurityException`( - @StringForgery message: String - ) { - // RUMM-852 in some cases the device throws a SecurityException on register - val context = mock() - val manager = mock() - val exception = SecurityException(message) - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception - - testedProvider.register(context) - - verify(mockDevLogHandler) - .handleLog( - Log.ERROR, - CallbackNetworkInfoProvider.ERROR_REGISTER, - exception - ) - } - - @Test - fun `M assume network is available W register() with SecurityException + getLatestNetworkInfo`( - @StringForgery message: String - ) { - // RUMM-852 in some cases the device throws a SecurityException on register - val context = mock() - val manager = mock() - val exception = SecurityException(message) - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - whenever(manager.registerDefaultNetworkCallback(testedProvider)) doThrow exception - - testedProvider.register(context) - val networkInfo = testedProvider.getLatestNetworkInfo() - - assertThat(networkInfo) - .hasConnectivity(NetworkInfo.Connectivity.NETWORK_OTHER) - .hasCarrierName(null) - .hasCarrierId(-1) - .hasUpSpeed(-1) - .hasDownSpeed(-1) - } - - @Test - fun `M unregister callback W unregister()`() { - val context = mock() - val manager = mock() - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - - testedProvider.unregister(context) - - verify(manager).unregisterNetworkCallback(testedProvider) - } - - @Test - fun `M unregister callback safely W unregister() with SecurityException`( - @StringForgery message: String - ) { - // RUMM-852 in some cases the device throws a SecurityException on register - // Since we can't reproduce, let's assume it could happen on unregister too - val context = mock() - val manager = mock() - val exception = SecurityException(message) - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - whenever(manager.unregisterNetworkCallback(testedProvider)) doThrow exception - - testedProvider.unregister(context) - - verify(manager).unregisterNetworkCallback(testedProvider) - } - - @Test - fun `M warn developers W unregister() with SecurityException`( - @StringForgery message: String - ) { - // RUMM-852 in some cases the device throws a SecurityException on register - val context = mock() - val manager = mock() - val exception = SecurityException(message) - whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) doReturn manager - whenever(manager.unregisterNetworkCallback(testedProvider)) doThrow exception - - testedProvider.unregister(context) - - verify(mockDevLogHandler) - .handleLog( - Log.ERROR, - CallbackNetworkInfoProvider.ERROR_UNREGISTER, - exception - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/sampling/RateBasedSamplerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/sampling/RateBasedSamplerTest.kt deleted file mode 100644 index 52d75d8502..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/sampling/RateBasedSamplerTest.kt +++ /dev/null @@ -1,100 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.sampling - -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Random -import kotlin.math.pow -import kotlin.math.sqrt -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RateBasedSamplerTest { - - lateinit var testedSampler: RateBasedSampler - - private var randomSampleRate: Float = 0.0f - - @BeforeEach - fun `set up`(forge: Forge) { - randomSampleRate = Random().nextFloat() - testedSampler = RateBasedSampler(randomSampleRate) - } - - @Test - fun `the sampler will sample the values based on the sample rate`(forge: Forge) { - val dataSize = 1000 - val testRepeats = 100 - val computedSamplingRates = mutableListOf() - repeat(testRepeats) { - var validated = 0 - repeat(dataSize) { - val isValid = if (testedSampler.sample()) 1 else 0 - validated += isValid - } - val computedSamplingRate = validated.toDouble() / dataSize.toDouble() - computedSamplingRates.add(computedSamplingRate) - } - val samplingRateMean = computedSamplingRates.sum().div(computedSamplingRates.size) - val variance = computedSamplingRates - .map { (samplingRateMean.minus(it)).pow(2) } - .sum() - .div(computedSamplingRates.size) - val deviation = sqrt(variance) - - assertThat(samplingRateMean).isCloseTo( - randomSampleRate.toDouble(), - Offset.offset(deviation) - ) - } - - @Test - fun `when sample rate is 0 all values will be dropped`(forge: Forge) { - testedSampler = RateBasedSampler(0.0f) - - var validated = 0 - val dataSize = 10 - - repeat(dataSize) { - val isValid = if (testedSampler.sample()) 1 else 0 - validated += isValid - } - - assertThat(validated).isEqualTo(0) - } - - @Test - fun `when sample rate is 1 all values will pass`(forge: Forge) { - testedSampler = RateBasedSampler(1.0f) - - var validated = 0 - val dataSize = 10 - - repeat(dataSize) { - val isValid = if (testedSampler.sample()) 1 else 0 - validated += isValid - } - - assertThat(validated).isEqualTo(dataSize) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProviderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProviderTest.kt deleted file mode 100644 index aa3de50971..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/system/BroadcastReceiverSystemInfoProviderTest.kt +++ /dev/null @@ -1,239 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.system - -import android.content.Context -import android.content.Intent -import android.os.BatteryManager -import android.os.Build -import android.os.PowerManager -import com.datadog.android.log.assertj.SystemInfoAssert.Companion.assertThat -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.same -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.BoolForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.RepeatedTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(value = Configurator::class, seed = 0xb1100d090b44cL) -internal class BroadcastReceiverSystemInfoProviderTest { - - lateinit var testedProvider: BroadcastReceiverSystemInfoProvider - - @Mock - lateinit var mockContext: Context - @Mock - lateinit var mockIntent: Intent - @Mock - lateinit var mockPowerMgr: PowerManager - - @BeforeEach - fun `set up`() { - whenever(mockContext.getSystemService(Context.POWER_SERVICE)) doReturn mockPowerMgr - - testedProvider = BroadcastReceiverSystemInfoProvider() - } - - @Test - fun `it will do nothing if unregister is called before register`() { - // When - testedProvider.unregister(mockContext) - - // Then - verifyZeroInteractions(mockContext) - } - - @Test - fun `it will unregister the receiver only once`() { - // Given - val countDownLatch = CountDownLatch(2) - testedProvider.register(mockContext) - - // When - Thread { - testedProvider.unregister(mockContext) - countDownLatch.countDown() - }.start() - Thread { - testedProvider.unregister(mockContext) - countDownLatch.countDown() - }.start() - - // Then - countDownLatch.await(3, TimeUnit.SECONDS) - verify(mockContext).unregisterReceiver(testedProvider) - } - - @Test - fun `initial state is unknown`() { - val systemInfo = testedProvider.getLatestSystemInfo() - - assertThat(systemInfo) - .hasBatteryLevel(-1) - .hasBatteryStatus(SystemInfo.BatteryStatus.UNKNOWN) - } - - @Test - @RepeatedTest(10) - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `read system info on register Lollipop`( - @Forgery status: SystemInfo.BatteryStatus, - @IntForgery(min = 0, max = 100) level: Int, - @IntForgery(min = 50, max = 10000) scale: Int, - @BoolForgery powerSaveMode: Boolean - ) { - val batteryIntent: Intent = mock() - val scaledLevel = (level * scale) / 100 - whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_STATUS), any())) - .doReturn(status.androidStatus()) - whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) - .doReturn(scaledLevel) - whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale - whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED - val powerSaveModeIntent: Intent = mock() - whenever(mockPowerMgr.isPowerSaveMode) doReturn powerSaveMode - whenever(powerSaveModeIntent.action) doReturn PowerManager.ACTION_POWER_SAVE_MODE_CHANGED - doReturn(batteryIntent, powerSaveModeIntent) - .whenever(mockContext).registerReceiver(same(testedProvider), any()) - - testedProvider.register(mockContext) - val systemInfo = testedProvider.getLatestSystemInfo() - - assertThat(systemInfo) - .hasBatteryLevel(level, scale) - .hasBatteryStatus(status) - .hasPowerSaveMode(powerSaveMode) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.KITKAT) - fun `read system info on register KitKat`( - @Forgery status: SystemInfo.BatteryStatus, - @IntForgery(min = 0, max = 100) level: Int, - @IntForgery(min = 50, max = 10000) scale: Int, - @BoolForgery powerSaveMode: Boolean - ) { - val batteryIntent: Intent = mock() - val scaledLevel = (level * scale) / 100 - whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_STATUS), any())) - .doReturn(status.androidStatus()) - whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) - .doReturn(scaledLevel) - whenever(batteryIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale - whenever(batteryIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED - - val powerSaveModeIntent: Intent = mock() - whenever(mockPowerMgr.isPowerSaveMode) doReturn powerSaveMode - whenever(powerSaveModeIntent.action) doReturn PowerManager.ACTION_POWER_SAVE_MODE_CHANGED - - whenever(mockContext.registerReceiver(same(testedProvider), any())) - .doReturn(batteryIntent, powerSaveModeIntent) - - testedProvider.register(mockContext) - val systemInfo = testedProvider.getLatestSystemInfo() - - assertThat(systemInfo) - .hasBatteryLevel(level, scale) - .hasBatteryStatus(status) - .hasPowerSaveMode(false) - } - - @Test - fun `battery changed (null)`() { - whenever(mockIntent.getIntExtra(any(), any())) doAnswer { - it.arguments[1] as Int - } - whenever(mockIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED - testedProvider.onReceive(mockContext, mockIntent) - - val systemInfo = testedProvider.getLatestSystemInfo() - - assertThat(systemInfo) - .hasBatteryLevel(-1) - .hasBatteryStatus(SystemInfo.BatteryStatus.UNKNOWN) - } - - @Test - fun `battery changed (not null)`( - @Forgery status: SystemInfo.BatteryStatus, - @IntForgery(min = 0, max = 100) level: Int, - @IntForgery(min = 50, max = 10000) scale: Int - ) { - val scaledLevel = (level * scale) / 100 - whenever(mockIntent.getIntExtra(eq(BatteryManager.EXTRA_STATUS), any())) - .doReturn(status.androidStatus()) - whenever(mockIntent.getIntExtra(eq(BatteryManager.EXTRA_LEVEL), any())) doReturn scaledLevel - whenever(mockIntent.getIntExtra(eq(BatteryManager.EXTRA_SCALE), any())) doReturn scale - whenever(mockIntent.action) doReturn Intent.ACTION_BATTERY_CHANGED - testedProvider.onReceive(mockContext, mockIntent) - - val systemInfo = testedProvider.getLatestSystemInfo() - - assertThat(systemInfo) - .hasBatteryLevel(level, scale) - .hasBatteryStatus(status) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `power save mode changed Lollipop`( - @BoolForgery powerSaveMode: Boolean - ) { - whenever(mockPowerMgr.isPowerSaveMode) doReturn powerSaveMode - whenever(mockIntent.action) doReturn PowerManager.ACTION_POWER_SAVE_MODE_CHANGED - testedProvider.onReceive(mockContext, mockIntent) - - val systemInfo = testedProvider.getLatestSystemInfo() - - assertThat(systemInfo) - .hasPowerSaveMode(powerSaveMode) - } - - // endregion - - // region Internal - - fun SystemInfo.BatteryStatus.androidStatus(): Int { - return when (this) { - SystemInfo.BatteryStatus.UNKNOWN -> BatteryManager.BATTERY_STATUS_UNKNOWN - SystemInfo.BatteryStatus.CHARGING -> BatteryManager.BATTERY_STATUS_CHARGING - SystemInfo.BatteryStatus.DISCHARGING -> BatteryManager.BATTERY_STATUS_DISCHARGING - SystemInfo.BatteryStatus.NOT_CHARGING -> BatteryManager.BATTERY_STATUS_NOT_CHARGING - SystemInfo.BatteryStatus.FULL -> BatteryManager.BATTERY_STATUS_FULL - } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/ByteArrayExtTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/ByteArrayExtTest.kt deleted file mode 100644 index 0e4b9906d0..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/ByteArrayExtTest.kt +++ /dev/null @@ -1,144 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings() -internal class ByteArrayExtTest { - - // region split - @Test - fun `splits a byteArray with 0 separator`(forge: Forge) { - val separationChar = forge.anAlphabeticalChar() - val rawString = forge.aNumericalString() - val byteArray = rawString.toByteArray(Charsets.UTF_8) - - val subs = byteArray.split(separationChar.toByte()) - - assertThat(subs).hasSize(1) - assertThat(subs[0]).isEqualTo(byteArray) - } - - @Test - fun `splits a byteArray with 1 separator`(forge: Forge) { - val separationChar = forge.anAlphabeticalChar() - val part0 = forge.aNumericalString() - val part1 = forge.aNumericalString() - val rawString = part0 + separationChar + part1 - val byteArray = rawString.toByteArray(Charsets.UTF_8) - - val subs = byteArray.split(separationChar.toByte()) - - assertThat(subs).hasSize(2) - assertThat(String(subs[0])).isEqualTo(part0) - assertThat(String(subs[1])).isEqualTo(part1) - } - - @Test - fun `splits a byteArray with trailing separator`(forge: Forge) { - val separationChar = forge.anAlphabeticalChar() - val part0 = forge.aNumericalString() - val rawString = part0 + separationChar - val byteArray = rawString.toByteArray(Charsets.UTF_8) - - val subs = byteArray.split(separationChar.toByte()) - - assertThat(subs).hasSize(1) - assertThat(String(subs[0])).isEqualTo(part0) - } - - @Test - fun `splits a byteArray with leading separator`(forge: Forge) { - val separationChar = forge.anAlphabeticalChar() - val part0 = forge.aNumericalString() - val rawString = separationChar + part0 - val byteArray = rawString.toByteArray(Charsets.UTF_8) - - val subs = byteArray.split(separationChar.toByte()) - - assertThat(subs).hasSize(1) - assertThat(String(subs[0])).isEqualTo(part0) - } - - @Test - fun `splits a byteArray with consecutive separators`(forge: Forge) { - val separationChar = forge.anAlphabeticalChar() - val part0 = forge.aNumericalString() - val part1 = forge.aNumericalString() - val rawString = part0 + separationChar + separationChar + part1 - val byteArray = rawString.toByteArray(Charsets.UTF_8) - - val subs = byteArray.split(separationChar.toByte()) - - assertThat(subs).hasSize(2) - assertThat(String(subs[0])).isEqualTo(part0) - assertThat(String(subs[1])).isEqualTo(part1) - } - - // endregion - - // region indexOf - - @Test - fun `returns -1 when byte not found`(forge: Forge) { - val rawString = forge.aNumericalString() - val byteArray = rawString.toByteArray(Charsets.UTF_8) - - val index = byteArray.indexOf(forge.anAlphabeticalChar().toByte(), 0) - - assertThat(index).isEqualTo(-1) - } - - @Test - fun `finds index of byte`(forge: Forge) { - val rawString = forge.anAsciiString() - val char = rawString[forge.anInt(0, rawString.length)] - val expectedIndex = rawString.indexOf(char) - - val byteArray = rawString.toByteArray(Charsets.UTF_8) - val index = byteArray.indexOf(char.toByte(), 0) - - assertThat(index).isEqualTo(expectedIndex) - } - - @Test - fun `finds all indexes of byte`(forge: Forge) { - val rawString = forge.aNumericalString(64) - val char = rawString[forge.anInt(0, rawString.length)] - val expectedIndexes = mutableListOf() - var nextExpectedIndex = rawString.indexOf(char, 0) - while (nextExpectedIndex != -1) { - expectedIndexes.add(nextExpectedIndex) - nextExpectedIndex = rawString.indexOf(char, nextExpectedIndex + 1) - } - - val byteArray = rawString.toByteArray(Charsets.UTF_8) - val foundIndexes = mutableListOf() - var nextIndex = byteArray.indexOf(char.toByte(), 0) - while (nextIndex != -1) { - foundIndexes.add(nextIndex) - nextIndex = byteArray.indexOf(char.toByte(), nextIndex + 1) - } - - assertThat(foundIndexes) - .containsAll(expectedIndexes) - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtilsTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtilsTest.kt deleted file mode 100644 index 26286d589e..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/MiscUtilsTest.kt +++ /dev/null @@ -1,145 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.forge.exhaustiveAttributes -import com.google.gson.JsonArray -import com.google.gson.JsonElement -import com.google.gson.JsonNull -import com.google.gson.JsonObject -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Date -import java.util.concurrent.TimeUnit -import kotlin.system.measureNanoTime -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) -internal class MiscUtilsTest { - - // region UnitTests - - @Test - fun `M repeat max N times W retryWithDelay { success = false }`(forge: Forge) { - // GIVEN - val fakeTimes = forge.anInt(min = 1, max = 10) - val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) - val mockedBlock: () -> Boolean = mock() - whenever(mockedBlock.invoke()).thenReturn(false) - - // WHEN - val wasSuccessful = retryWithDelay(mockedBlock, fakeTimes, fakeDelay) - - // THEN - assertThat(wasSuccessful).isFalse() - verify(mockedBlock, times(fakeTimes)).invoke() - } - - @Test - fun `M execute the block in a delayed loop W retryWithDelay`(forge: Forge) { - // GIVEN - val fakeTimes = forge.anInt(min = 1, max = 4) - val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) - val mockedBlock: () -> Boolean = mock() - whenever(mockedBlock.invoke()).thenReturn(false) - - // WHEN - val executionTime = measureNanoTime { retryWithDelay(mockedBlock, fakeTimes, fakeDelay) } - - // THEN - assertThat(executionTime).isCloseTo( - fakeTimes * fakeDelay, - Offset.offset(TimeUnit.SECONDS.toNanos(1)) - ) - } - - @Test - fun `M do nothing W retryWithDelay { times less or equal than 0 }`(forge: Forge) { - // GIVEN - val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) - val mockedBlock: () -> Boolean = mock() - - // WHEN - retryWithDelay(mockedBlock, forge.anInt(Int.MIN_VALUE, 1), fakeDelay) - - // THEN - verifyZeroInteractions(mockedBlock) - } - - @Test - fun `M repeat until success W retryWithDelay`(forge: Forge) { - // GIVEN - val fakeDelay = TimeUnit.SECONDS.toNanos(forge.aLong(min = 0, max = 2)) - val mockedBlock: () -> Boolean = mock() - whenever(mockedBlock.invoke()).thenReturn(false).thenReturn(true) - - // WHEN - val wasSuccessful = retryWithDelay(mockedBlock, 3, fakeDelay) - - // THEN - assertThat(wasSuccessful).isTrue() - verify(mockedBlock, times(2)).invoke() - } - - @Test - fun `M provide the relevant JsonElement W toJsonElement { on Kotlin object }`(forge: Forge) { - // GIVEN - val attributes = forge.exhaustiveAttributes().toMutableMap() - attributes[forge.aString()] = NULL_MAP_VALUE - - // WHEN - attributes.forEach { - val jsonElement = it.toJsonElement() - assertJsonElement(it, jsonElement) - } - } - - // endregion - - // region Internal - - private fun assertJsonElement(kotlinObject: Any?, jsonElement: JsonElement) { - when (kotlinObject) { - NULL_MAP_VALUE -> assertThat(jsonElement).isEqualTo(JsonNull.INSTANCE) - null -> assertThat(jsonElement).isEqualTo(JsonNull.INSTANCE) - is Boolean -> assertThat(jsonElement.asBoolean).isEqualTo(kotlinObject) - is Int -> assertThat(jsonElement.asInt).isEqualTo(kotlinObject) - is Long -> assertThat(jsonElement.asLong).isEqualTo(kotlinObject) - is Float -> assertThat(jsonElement.asFloat).isEqualTo(kotlinObject) - is Double -> assertThat(jsonElement.asDouble).isEqualTo(kotlinObject) - is String -> assertThat(jsonElement.asString).isEqualTo(kotlinObject) - is Date -> assertThat(jsonElement.asLong).isEqualTo(kotlinObject.time) - is JsonObject -> assertThat(jsonElement.asJsonObject).isEqualTo(kotlinObject) - is JsonArray -> assertThat(jsonElement.asJsonArray).isEqualTo(kotlinObject) - is Iterable<*> -> assertThat(jsonElement.asJsonArray).containsExactlyElementsOf( - kotlinObject.map { it.toJsonElement() } - ) - else -> assertThat(jsonElement.asString).isEqualTo(kotlinObject.toString()) - } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/RuntimeUtilsTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/RuntimeUtilsTest.kt deleted file mode 100644 index 6c75f2fbc5..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/RuntimeUtilsTest.kt +++ /dev/null @@ -1,92 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import android.util.Log -import com.datadog.android.Datadog -import com.datadog.android.log.internal.logger.ConditionalLogHandler -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.log.internal.logger.LogcatLogHandler -import com.datadog.android.log.internal.logger.NoOpLogHandler -import com.datadog.android.utils.extension.EnableLogcat -import com.datadog.android.utils.extension.EnableLogcatExtension -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.getFieldValue -import com.datadog.tools.unit.setFieldValue -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(EnableLogcatExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -class RuntimeUtilsTest { - - @BeforeEach - fun `set up`() { - devLogger.setFieldValue("handler", buildDevLogHandler()) - } - - @AfterEach - fun `tear down`() { - Datadog.setFieldValue("isDebug", false) - } - - @Test - @EnableLogcat(isEnabled = false) - fun `the sdk logger should disable the logcat logs if the BuildConfig flag is false`() { - val logger = buildSdkLogger() - val handler: LogHandler = logger.getFieldValue("handler") - assertThat(handler).isInstanceOf(NoOpLogHandler::class.java) - } - - @Test - @EnableLogcat(isEnabled = true) - fun `the sdk logger should enable the logcat logs if the BuildConfig flag is true`() { - val logger = buildSdkLogger() - val handler: LogHandler = logger.getFieldValue("handler") - assertThat(handler).isInstanceOf(LogcatLogHandler::class.java) - assertThat((handler as LogcatLogHandler).serviceName) - .isEqualTo(SDK_LOG_PREFIX) - } - - @Test - fun `the dev logger should always be enabled`() { - val handler: LogHandler = devLogger.getFieldValue("handler") - assertThat(handler).isInstanceOf(ConditionalLogHandler::class.java) - assertThat((handler as ConditionalLogHandler).delegateHandler) - .isInstanceOf(LogcatLogHandler::class.java) - } - - @Test - fun `the dev logger handler should use the Datadog verbosity level`( - @IntForgery(min = Log.VERBOSE, max = (Log.ASSERT + 1)) level: Int - ) { - Datadog.setVerbosity(level) - val handler: LogHandler = devLogger.getFieldValue("handler") - assertThat(handler).isInstanceOf(ConditionalLogHandler::class.java) - - val condition = (handler as ConditionalLogHandler).condition - - for (i in 0..10) { - if (i >= level) { - assertThat(condition(i, null)).isTrue() - } else { - assertThat(condition(i, null)).isFalse() - } - } - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/ThrowableExtTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/ThrowableExtTest.kt deleted file mode 100644 index e46b19ecd6..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/ThrowableExtTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.datadog.android.core.internal.utils - -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.PrintWriter -import java.io.StringWriter -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -internal class ThrowableExtTest { - - @Forgery - lateinit var throwable: Throwable - - @Test - fun `get loggable stack trace`() { - val sw = StringWriter() - val pw = PrintWriter(sw) - throwable.printStackTrace(pw) - - assertThat(throwable.loggableStackTrace()) - .isEqualTo(sw.toString()) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtilsTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtilsTest.kt deleted file mode 100644 index 621afea6b1..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/utils/WorkManagerUtilsTest.kt +++ /dev/null @@ -1,116 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.utils - -import android.app.Application -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.impl.WorkManagerImpl -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.data.upload.UploadWorker -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.invokeMethod -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class WorkManagerUtilsTest { - - @Mock - lateinit var mockWorkManager: WorkManagerImpl - - lateinit var mockAppContext: Application - - @BeforeEach - fun `set up`(forge: Forge) { - mockAppContext = mockContext() - Datadog.initialize( - mockAppContext, - DatadogConfig.Builder( - forge.anAlphabeticalString(), - forge.anHexadecimalString() - ).build() - ) - } - - @AfterEach - fun `tear down`() { - Datadog.invokeMethod("stop") - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", null) - } - - @Test - fun `it will cancel the worker if WorkManager was correctly instantiated`() { - // Given - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) - - // When - cancelUploadWorker(mockContext()) - - // Then - verify(mockWorkManager).cancelAllWorkByTag(eq(TAG_DATADOG_UPLOAD)) - } - - @Test - fun `it will handle the cancel exception if WorkManager was not correctly instantiated`() { - // When - cancelUploadWorker(mockContext()) - - // Then - verifyZeroInteractions(mockWorkManager) - } - - @Test - fun `it will schedule the worker if WorkManager was correctly instantiated`() { - // Given - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) - - // When - triggerUploadWorker(mockContext()) - - // Then - verify(mockWorkManager).enqueueUniqueWork( - eq(UPLOAD_WORKER_NAME), - eq(ExistingWorkPolicy.REPLACE), - argThat { - this.workSpec.workerClassName == UploadWorker::class.java.canonicalName && - this.tags.contains(TAG_DATADOG_UPLOAD) - } - ) - } - - @Test - fun `it will handle the trigger exception if WorkManager was not correctly instantiated`() { - // When - triggerUploadWorker(mockContext()) - - // Then - verifyZeroInteractions(mockWorkManager) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/CrashLogFileStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/CrashLogFileStrategyTest.kt deleted file mode 100644 index 42ab79b2ac..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/CrashLogFileStrategyTest.kt +++ /dev/null @@ -1,74 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.error.internal - -import android.content.Context -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.assertj.PersistenceStrategyAssert -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.ExecutorService -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class CrashLogFileStrategyTest { - lateinit var testedStrategy: CrashLogFileStrategy - - lateinit var mockedContext: Context - - @Mock - lateinit var mockExecutorService: ExecutorService - - lateinit var trackingConsentProvider: TrackingConsentProvider - - @BeforeEach - fun `set up`() { - mockedContext = mockContext() - trackingConsentProvider = TrackingConsentProvider() - testedStrategy = CrashLogFileStrategy( - mockedContext, - dataPersistenceExecutorService = mockExecutorService, - trackingConsentProvider = trackingConsentProvider - ) - } - - @Test - fun `M correctly initialise the strategy W instantiated`() { - val absolutePath = mockedContext.filesDir.absolutePath - val expectedIntermediateFolderPath = - absolutePath + - File.separator + - CrashLogFileStrategy.INTERMEDIATE_DATA_FOLDER - val expectedAuthorizedFolderPath = - absolutePath + - File.separator + - CrashLogFileStrategy.AUTHORIZED_FOLDER - PersistenceStrategyAssert - .assertThat(testedStrategy) - .hasIntermediateStorageFolder(expectedIntermediateFolderPath) - .hasAuthorizedStorageFolder(expectedAuthorizedFolderPath) - .uploadsFrom(expectedAuthorizedFolderPath) - .usesImmediateWriter() - .hasConfig(FilePersistenceConfig(CrashLogFileStrategy.MAX_DELAY_BETWEEN_LOGS_MS)) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/CrashReportsFeatureTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/CrashReportsFeatureTest.kt deleted file mode 100644 index 1bb9626e4f..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/CrashReportsFeatureTest.kt +++ /dev/null @@ -1,439 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.error.internal - -import android.app.Application -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.data.upload.DataUploadScheduler -import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler -import com.datadog.android.core.internal.domain.FilePersistenceStrategy -import com.datadog.android.core.internal.net.DataOkHttpUploader -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.DatadogPluginConfig -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.net.URL -import java.util.concurrent.ExecutorService -import java.util.concurrent.ScheduledThreadPoolExecutor -import okhttp3.OkHttpClient -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class CrashReportsFeatureTest { - - lateinit var mockAppContext: Application - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @Mock - lateinit var mockSystemInfoProvider: SystemInfoProvider - - @Mock - lateinit var mockOkHttpClient: OkHttpClient - - @Mock - lateinit var mockScheduledThreadPoolExecutor: ScheduledThreadPoolExecutor - - @Mock - lateinit var mockedPersistenceExecutorService: ExecutorService - - lateinit var trackingConsentProvider: ConsentProvider - - lateinit var fakeConfig: DatadogConfig.FeatureConfig - - lateinit var fakePackageName: String - lateinit var fakePackageVersion: String - - @TempDir - lateinit var tempRootDir: File - - @BeforeEach - fun `set up`(forge: Forge) { - CoreFeature.isMainProcess = true - trackingConsentProvider = TrackingConsentProvider() - fakeConfig = DatadogConfig.FeatureConfig( - clientToken = forge.anHexadecimalString(), - applicationId = forge.getForgery(), - endpointUrl = forge.getForgery().toString(), - envName = forge.anAlphabeticalString() - ) - - fakePackageName = forge.anAlphabeticalString() - fakePackageVersion = forge.aStringMatching("\\d(\\.\\d){3}") - - mockAppContext = mockContext(fakePackageName, fakePackageVersion) - whenever(mockAppContext.filesDir).thenReturn(tempRootDir) - whenever(mockAppContext.applicationContext) doReturn mockAppContext - } - - @AfterEach - fun `tear down`() { - CoreFeature.stop() - CrashReportsFeature.stop() - } - - @Test - fun `initializes persistence strategy`() { - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - - val persistenceStrategy = CrashReportsFeature.persistenceStrategy - - assertThat(persistenceStrategy) - .isInstanceOf(FilePersistenceStrategy::class.java) - } - - @Test - fun `initializes uploader thread`() { - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - - val uploader = CrashReportsFeature.uploader - val dataUploadScheduler = CrashReportsFeature.dataUploadScheduler - - assertThat(uploader) - .isInstanceOf(DataOkHttpUploader::class.java) - assertThat(dataUploadScheduler) - .isInstanceOf(DataUploadScheduler::class.java) - } - - @Test - fun `initializes from configuration`() { - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - - val clientToken = CrashReportsFeature.clientToken - val endpointUrl = CrashReportsFeature.endpointUrl - - assertThat(clientToken).isEqualTo(fakeConfig.clientToken) - assertThat(endpointUrl).isEqualTo(fakeConfig.endpointUrl) - } - - @Test - fun `initializes crash reporter`() { - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - - val handler = Thread.getDefaultUncaughtExceptionHandler() - - assertThat(handler) - .isInstanceOf(DatadogExceptionHandler::class.java) - } - - @Test - fun `restores original crash reporter on stop`() { - val originalHandler = Thread.getDefaultUncaughtExceptionHandler() - val handler: Thread.UncaughtExceptionHandler = mock() - Thread.setDefaultUncaughtExceptionHandler(handler) - - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - CrashReportsFeature.stop() - - val finalHandler = Thread.getDefaultUncaughtExceptionHandler() - assertThat(finalHandler) - .isSameAs(handler) - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - @Test - fun `ignores if initialize called more than once`(forge: Forge) { - Datadog.setVerbosity(android.util.Log.VERBOSE) - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - val persistenceStrategy = CrashReportsFeature.persistenceStrategy - val uploader = CrashReportsFeature.uploader - val clientToken = CrashReportsFeature.clientToken - val endpointUrl = CrashReportsFeature.endpointUrl - - fakeConfig = DatadogConfig.FeatureConfig( - clientToken = forge.anHexadecimalString(), - applicationId = forge.getForgery(), - endpointUrl = forge.getForgery().toString(), - envName = forge.anAlphabeticalString() - ) - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - val persistenceStrategy2 = CrashReportsFeature.persistenceStrategy - val uploader2 = CrashReportsFeature.uploader - val clientToken2 = CrashReportsFeature.clientToken - val endpointUrl2 = CrashReportsFeature.endpointUrl - - assertThat(persistenceStrategy).isSameAs(persistenceStrategy2) - assertThat(uploader).isSameAs(uploader2) - assertThat(clientToken).isSameAs(clientToken2) - assertThat(endpointUrl).isSameAs(endpointUrl2) - } - - @Test - fun `it will register the provided plugin when feature is initialized`( - forge: Forge - ) { - // Given - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - val mockedTrackingConsentProvider: TrackingConsentProvider = mock() { - whenever(it.getConsent()).thenReturn(fakeConsent) - } - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - - // When - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - mockedTrackingConsentProvider - ) - - val argumentCaptor = argumentCaptor() - - // Then - val mockPlugins = plugins.toTypedArray() - inOrder(*mockPlugins) { - mockPlugins.forEach { - verify(it).register(argumentCaptor.capture()) - } - } - - argumentCaptor.allValues.forEach { - assertThat(it).isInstanceOf(DatadogPluginConfig.CrashReportsPluginConfig::class.java) - assertThat(it.context).isEqualTo(mockAppContext) - assertThat(it.serviceName).isEqualTo(CoreFeature.serviceName) - assertThat(it.envName).isEqualTo(fakeConfig.envName) - assertThat(it.featurePersistenceDirName) - .isEqualTo(CrashLogFileStrategy.AUTHORIZED_FOLDER) - assertThat(it.context).isEqualTo(mockAppContext) - assertThat(it.trackingConsent).isEqualTo(fakeConsent) - } - } - - @Test - fun `M register the plugins as TrackingConsentProvideCallback W initialized`( - forge: Forge - ) { - // Given - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - val mockedTrackingConsentProvider: TrackingConsentProvider = mock() { - whenever(it.getConsent()).thenReturn(fakeConsent) - } - - // When - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - mockedTrackingConsentProvider - ) - - // Then - val mockPlugins = plugins.toTypedArray() - mockPlugins.forEach { - verify(mockedTrackingConsentProvider).registerCallback(it) - } - } - - @Test - fun `M unregister the provided plugin W stop called`( - forge: Forge - ) { - // Given - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - // When - CrashReportsFeature.stop() - - // Then - val mockPlugins = plugins.toTypedArray() - inOrder(*mockPlugins) { - mockPlugins.forEach { - verify(it).unregister() - } - } - } - - @Test - fun `will use a NoOpUploadScheduler if this is not the application main process`() { - // Given - CoreFeature.isMainProcess = false - - // When - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - - // Then - assertThat(CrashReportsFeature.dataUploadScheduler) - .isInstanceOf(NoOpUploadScheduler::class.java) - } - - @Test - fun `clears all files on local storage on request`( - @StringForgery(type = StringForgeryType.NUMERICAL) fileName: String, - @StringForgery content: String - ) { - val fakeDir = File(tempRootDir, CrashLogFileStrategy.AUTHORIZED_FOLDER) - fakeDir.mkdirs() - val fakeFile = File(fakeDir, fileName) - fakeFile.writeText(content) - - // When - CrashReportsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockUserInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockedPersistenceExecutorService, - trackingConsentProvider - ) - CrashReportsFeature.clearAllData() - - // Then - assertThat(fakeFile).doesNotExist() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt deleted file mode 100644 index fbf509129f..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt +++ /dev/null @@ -1,332 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.error.internal - -import android.app.Application -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.impl.WorkManagerImpl -import com.datadog.android.BuildConfig -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.data.upload.UploadWorker -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.utils.TAG_DATADOG_UPLOAD -import com.datadog.android.core.internal.utils.UPLOAD_WORKER_NAME -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.assertj.LogAssert.Companion.assertThat -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.domain.LogGenerator -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.tracing.AndroidTracer -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.invokeMethod -import com.datadog.tools.unit.setFieldValue -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.noop.NoopTracerFactory -import io.opentracing.util.GlobalTracer -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogExceptionHandlerTest { - - var originalHandler: Thread.UncaughtExceptionHandler? = null - - lateinit var testedHandler: DatadogExceptionHandler - - @Mock - lateinit var mockPreviousHandler: Thread.UncaughtExceptionHandler - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @Mock - lateinit var mockLogWriter: Writer - - @Mock - lateinit var mockWorkManager: WorkManagerImpl - - @Mock - lateinit var mockRumMonitor: AdvancedRumMonitor - - @Forgery - lateinit var fakeThrowable: Throwable - - @Forgery - lateinit var fakeNetworkInfo: NetworkInfo - - @Forgery - lateinit var fakeUserInfo: UserInfo - - @StringForgery(StringForgeryType.HEXADECIMAL) - lateinit var fakeToken: String - - @RegexForgery("[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]") - lateinit var fakeEnvName: String - - @BeforeEach - fun `set up`() { - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - val mockContext: Application = mockContext() - val config = DatadogConfig.Builder(fakeToken, fakeEnvName).build() - Datadog.initialize(mockContext(), config) - - originalHandler = Thread.getDefaultUncaughtExceptionHandler() - Thread.setDefaultUncaughtExceptionHandler(mockPreviousHandler) - testedHandler = DatadogExceptionHandler( - LogGenerator( - CoreFeature.serviceName, - DatadogExceptionHandler.LOGGER_NAME, - mockNetworkInfoProvider, - mockUserInfoProvider, - CoreFeature.envName, - CoreFeature.packageVersion - ), - writer = mockLogWriter, - appContext = mockContext - ) - testedHandler.register() - } - - @AfterEach - fun `tear down`() { - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", null) - Datadog.invokeMethod("stop") - GlobalTracer.get().setFieldValue("isRegistered", false) - GlobalTracer::class.java.setStaticValue("tracer", NoopTracerFactory.create()) - GlobalRum.isRegistered.set(false) - GlobalRum.monitor = NoOpRumMonitor() - } - - @Test - fun `M log exception W caught with no previous handler`(forge: Forge) { - Thread.setDefaultUncaughtExceptionHandler(null) - testedHandler.register() - val currentThread = Thread.currentThread() - - val now = System.currentTimeMillis() - testedHandler.uncaughtException(currentThread, fakeThrowable) - - argumentCaptor { - verify(mockLogWriter).write(capture()) - assertThat(lastValue) - .hasThreadName(currentThread.name) - .hasMessage("Application crash detected") - .hasLevel(Log.CRASH) - .hasThrowable(fakeThrowable) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasTimestampAround(now) - .hasExactlyTags( - listOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:${BuildConfig.VERSION_NAME}" - ) - ) - .hasExactlyAttributes(emptyMap()) - } - verifyZeroInteractions(mockPreviousHandler) - } - - @Test - fun `M schedule the worker W logging an exception`(forge: Forge) { - WorkManagerImpl::class.java.setStaticValue("sDefaultInstance", mockWorkManager) - Thread.setDefaultUncaughtExceptionHandler(null) - testedHandler.register() - val currentThread = Thread.currentThread() - - val now = System.currentTimeMillis() - testedHandler.uncaughtException(currentThread, fakeThrowable) - - verify(mockWorkManager) - .enqueueUniqueWork( - eq(UPLOAD_WORKER_NAME), - eq(ExistingWorkPolicy.REPLACE), - argThat { - this.workSpec.workerClassName == UploadWorker::class.java.canonicalName && - this.tags.contains(TAG_DATADOG_UPLOAD) - } - ) - } - - @Test - fun `M log exception W caught`(forge: Forge) { - val currentThread = Thread.currentThread() - - val now = System.currentTimeMillis() - testedHandler.uncaughtException(currentThread, fakeThrowable) - - argumentCaptor { - verify(mockLogWriter).write(capture()) - assertThat(lastValue) - .hasThreadName(currentThread.name) - .hasMessage("Application crash detected") - .hasLevel(Log.CRASH) - .hasThrowable(fakeThrowable) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasTimestampAround(now) - .hasExactlyTags( - listOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:${BuildConfig.VERSION_NAME}" - ) - ) - .hasExactlyAttributes(emptyMap()) - } - verify(mockPreviousHandler).uncaughtException(currentThread, fakeThrowable) - } - - @Test - fun `M log exception W caught on background thread`(forge: Forge) { - val latch = CountDownLatch(1) - val threadName = forge.anAlphabeticalString() - val thread = Thread( - { - testedHandler.uncaughtException(Thread.currentThread(), fakeThrowable) - latch.countDown() - }, - threadName - ) - - val now = System.currentTimeMillis() - thread.start() - latch.await(1, TimeUnit.SECONDS) - - argumentCaptor { - verify(mockLogWriter).write(capture()) - assertThat(lastValue) - .hasThreadName(threadName) - .hasMessage("Application crash detected") - .hasLevel(Log.CRASH) - .hasThrowable(fakeThrowable) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasTimestampAround(now) - .hasExactlyTags( - listOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:${BuildConfig.VERSION_NAME}" - ) - ) - .hasExactlyAttributes(emptyMap()) - } - verify(mockPreviousHandler).uncaughtException(thread, fakeThrowable) - } - - @Test - fun `M add current span information W tracer is active`( - @StringForgery operation: String - ) { - val currentThread = Thread.currentThread() - val tracer = AndroidTracer.Builder().build() - val span = tracer.buildSpan(operation).start() - tracer.activateSpan(span) - GlobalTracer.registerIfAbsent(tracer) - - testedHandler.uncaughtException(currentThread, fakeThrowable) - - argumentCaptor { - verify(mockLogWriter).write(capture()) - - assertThat(lastValue) - .hasExactlyAttributes( - mapOf( - LogAttributes.DD_TRACE_ID to tracer.traceId, - LogAttributes.DD_SPAN_ID to tracer.spanId - ) - ) - } - Datadog.invokeMethod("stop") - } - - @Test - fun `M register RUM Error with crash W RumMonitor registered`() { - val currentThread = Thread.currentThread() - GlobalRum.registerIfAbsent(mockRumMonitor) - - testedHandler.uncaughtException(currentThread, fakeThrowable) - - verify(mockRumMonitor).addCrash( - "Application crash detected", - RumErrorSource.SOURCE, - fakeThrowable - ) - verify(mockPreviousHandler).uncaughtException(currentThread, fakeThrowable) - } - - @Test - fun `M add current RUM information W GlobalRum is active`( - @Forgery rumContext: RumContext - ) { - val currentThread = Thread.currentThread() - GlobalRum.updateRumContext(rumContext) - GlobalRum.registerIfAbsent(mockRumMonitor) - - testedHandler.uncaughtException(currentThread, fakeThrowable) - - argumentCaptor { - verify(mockLogWriter).write(capture()) - - assertThat(lastValue) - .hasExactlyAttributes( - mapOf( - LogAttributes.RUM_APPLICATION_ID to rumContext.applicationId, - LogAttributes.RUM_SESSION_ID to rumContext.sessionId, - LogAttributes.RUM_VIEW_ID to rumContext.viewId - ) - ) - } - verify(mockPreviousHandler).uncaughtException(currentThread, fakeThrowable) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/LoggerBuilderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/LoggerBuilderTest.kt deleted file mode 100644 index 2e4ec6b127..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/LoggerBuilderTest.kt +++ /dev/null @@ -1,215 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log - -import android.content.Context -import android.util.Log as AndroidLog -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.sampling.RateBasedSampler -import com.datadog.android.log.internal.logger.CombinedLogHandler -import com.datadog.android.log.internal.logger.DatadogLogHandler -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.log.internal.logger.LogcatLogHandler -import com.datadog.android.log.internal.logger.NoOpLogHandler -import com.datadog.android.utils.mockContext -import com.datadog.android.utils.mockDevLogHandler -import com.datadog.tools.unit.getFieldValue -import com.datadog.tools.unit.invokeMethod -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings() -internal class LoggerBuilderTest { - - lateinit var mockContext: Context - - lateinit var fakePackageName: String - - @TempDir - lateinit var tempRootDir: File - - @BeforeEach - fun `set up Datadog`(forge: Forge) { - fakePackageName = forge.anAlphabeticalString() - mockContext = mockContext(fakePackageName, "") - whenever(mockContext.filesDir) doReturn tempRootDir - - Datadog.initialize( - mockContext, - DatadogConfig.Builder(forge.anAlphabeticalString(), forge.anHexadecimalString()).build() - ) - Datadog.setVerbosity(AndroidLog.VERBOSE) - } - - @AfterEach - fun `tear down Datadog`() { - try { - Datadog.invokeMethod("stop") - } catch (e: IllegalStateException) { - // ignore - } - } - - @Test - fun `builder returns no op if SDK is not initialized`() { - val mockDevLogHandler = mockDevLogHandler() - Datadog.invokeMethod("stop") // simulate non initialized SDK - val logger = Logger.Builder() - .build() - - val handler: LogHandler = logger.getFieldValue("handler") - - assertThat(handler).isInstanceOf(NoOpLogHandler::class.java) - verify(mockDevLogHandler) - .handleLog(AndroidLog.ERROR, Datadog.MESSAGE_NOT_INITIALIZED) - } - - @Test - fun `builder without custom settings uses defaults`() { - val logger = Logger.Builder() - .build() - - val handler: DatadogLogHandler = logger.getFieldValue("handler") - assertThat(handler.logGenerator.serviceName).isEqualTo(CoreFeature.serviceName) - assertThat(handler.logGenerator.loggerName).isEqualTo(fakePackageName) - assertThat(handler.logGenerator.networkInfoProvider).isNull() - assertThat(handler.writer).isNotNull() - assertThat(handler.bundleWithTraces).isTrue() - assertThat(handler.sampler).isInstanceOf(RateBasedSampler::class.java) - assertThat((handler.sampler as RateBasedSampler).sampleRate).isEqualTo(1.0f) - } - - @Test - fun `builder can set a ServiceName`(@Forgery forge: Forge) { - val serviceName = forge.anAlphabeticalString() - - val logger = Logger.Builder() - .setServiceName(serviceName) - .build() - - val handler: DatadogLogHandler = logger.getFieldValue("handler") - assertThat(handler.logGenerator.serviceName).isEqualTo(serviceName) - } - - @Test - fun `builder can disable datadog logs`(@Forgery forge: Forge) { - val datadogLogsEnabled = false - - val logger: Logger = Logger.Builder() - .setDatadogLogsEnabled(datadogLogsEnabled) - .build() - - val handler: LogHandler = logger.getFieldValue("handler") - assertThat(handler).isInstanceOf(NoOpLogHandler::class.java) - } - - @Test - fun `builder can enable logcat logs`( - @Forgery forge: Forge - ) { - val logcatLogsEnabled = true - - val logger = Logger.Builder() - .setLogcatLogsEnabled(logcatLogsEnabled) - .build() - - val handler: LogHandler = logger.getFieldValue("handler") - assertThat(handler).isInstanceOf(CombinedLogHandler::class.java) - val handlers = (handler as CombinedLogHandler).handlers - assertThat(handlers) - .hasAtLeastOneElementOfType(LogcatLogHandler::class.java) - .hasAtLeastOneElementOfType(DatadogLogHandler::class.java) - } - - @Test - fun `builder can enable only logcat logs`( - @Forgery forge: Forge - ) { - val logcatLogsEnabled = true - val fakeServiceName = forge.anAlphaNumericalString() - - val logger = Logger.Builder() - .setDatadogLogsEnabled(false) - .setLogcatLogsEnabled(logcatLogsEnabled) - .setServiceName(fakeServiceName) - .build() - - val handler: LogHandler = logger.getFieldValue("handler") - assertThat(handler).isInstanceOf(LogcatLogHandler::class.java) - val logcatLogHandler = handler as LogcatLogHandler - assertThat(logcatLogHandler.serviceName) - .isEqualTo(fakeServiceName) - assertThat(logcatLogHandler.useClassnameAsTag) - .isTrue() - } - - @Test - fun `builder can enable network info`(@Forgery forge: Forge) { - val networkInfoEnabled = true - - val logger = Logger.Builder() - .setNetworkInfoEnabled(networkInfoEnabled) - .build() - - val handler: DatadogLogHandler = logger.getFieldValue("handler") - assertThat(handler.logGenerator.networkInfoProvider).isNotNull() - } - - @Test - fun `builder can set the logger name`(@Forgery forge: Forge) { - val loggerName = forge.anAlphabeticalString() - - val logger = Logger.Builder() - .setLoggerName(loggerName) - .build() - - val handler: DatadogLogHandler = logger.getFieldValue("handler") - assertThat(handler.logGenerator.loggerName).isEqualTo(loggerName) - } - - @Test - fun `buider can disable the bundle with trace feature`(@Forgery forge: Forge) { - val logger = Logger.Builder() - .setBundleWithTraceEnabled(false) - .build() - - val handler: DatadogLogHandler = logger.getFieldValue("handler") - assertThat(handler.bundleWithTraces).isFalse() - } - - @Test - fun `builder can set a sampling rate`(@Forgery forge: Forge) { - val expectedSampleRate = forge.aFloat(min = 0.0f, max = 1.0f) - - val logger = Logger.Builder().setSampleRate(expectedSampleRate).build() - - val handler: DatadogLogHandler = logger.getFieldValue("handler") - val sampler = handler.sampler - assertThat(sampler).isInstanceOf(RateBasedSampler::class.java) - assertThat((sampler as RateBasedSampler).sampleRate).isEqualTo(expectedSampleRate) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/LoggerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/LoggerTest.kt deleted file mode 100644 index 1b10192610..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/LoggerTest.kt +++ /dev/null @@ -1,974 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log - -import android.util.Log -import com.datadog.android.core.internal.utils.NULL_MAP_VALUE -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.utils.forge.Configurator -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.isNull -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Date -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings -@ForgeConfiguration(Configurator::class) -internal class LoggerTest { - - lateinit var testedLogger: Logger - - @Mock - lateinit var mockLogHandler: LogHandler - - lateinit var fakeMessage: String - - @BeforeEach - fun `set up`(forge: Forge) { - fakeMessage = forge.anAlphabeticalString() - testedLogger = Logger(mockLogHandler) - } - - // region Log - - @Test - fun `logger logs message with verbose level`() { - testedLogger.v(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.VERBOSE, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `logger logs message with verbose level and custom timestamp`(forge: Forge) { - val timestamp = forge.aLong() - val level = forge.anInt() - testedLogger.internalLog(level, fakeMessage, null, emptyMap(), timestamp) - - verify(mockLogHandler) - .handleLog( - level, - fakeMessage, - null, - emptyMap(), - emptySet(), - timestamp - ) - } - - @Test - fun `logger logs message with debug level`() { - testedLogger.d(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.DEBUG, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `logger logs message with info level`() { - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `logger logs message with warning level`() { - testedLogger.w(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.WARN, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `logger logs message with error level`() { - testedLogger.e(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.ERROR, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `logger logs message with assert level`() { - testedLogger.wtf(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.ASSERT, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - // endregion - - // region throwable - @Test - fun `log verbose with exception`(@Forgery throwable: Throwable) { - testedLogger.v(fakeMessage, throwable) - - verify(mockLogHandler) - .handleLog( - Log.VERBOSE, - fakeMessage, - throwable, - emptyMap(), - emptySet() - ) - } - - @Test - fun `log debug with exception`(@Forgery throwable: Throwable) { - testedLogger.d(fakeMessage, throwable) - - verify(mockLogHandler) - .handleLog( - Log.DEBUG, - fakeMessage, - throwable, - emptyMap(), - emptySet() - ) - } - - @Test - fun `log info with exception`(@Forgery throwable: Throwable) { - testedLogger.i(fakeMessage, throwable) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - throwable, - emptyMap(), - emptySet() - ) - } - - @Test - fun `log warning with exception`(@Forgery throwable: Throwable) { - testedLogger.w(fakeMessage, throwable) - - verify(mockLogHandler) - .handleLog( - Log.WARN, - fakeMessage, - throwable, - emptyMap(), - emptySet() - ) - } - - @Test - fun `log error with exception`(@Forgery throwable: Throwable) { - testedLogger.e(fakeMessage, throwable) - - verify(mockLogHandler) - .handleLog( - Log.ERROR, - fakeMessage, - throwable, - emptyMap(), - emptySet() - ) - } - - @Test - fun `log wtf with exception`(@Forgery throwable: Throwable) { - testedLogger.wtf(fakeMessage, throwable) - - verify(mockLogHandler) - .handleLog( - Log.ASSERT, - fakeMessage, - throwable, - emptyMap(), - emptySet() - ) - } - - // endregion - - // region addAttribute - - @Test - fun `add boolean attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aBool() - - testedLogger.addAttribute(key, value) - testedLogger.v(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.VERBOSE, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add int attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.anInt() - - testedLogger.addAttribute(key, value) - testedLogger.d(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.DEBUG, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add long attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aLong() - - testedLogger.addAttribute(key, value) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add float attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aFloat() - - testedLogger.addAttribute(key, value) - testedLogger.w(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.WARN, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add double attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aDouble() - - testedLogger.addAttribute(key, value) - testedLogger.e(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.ERROR, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add String attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aNumericalString() - - testedLogger.addAttribute(key, value) - testedLogger.wtf(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.ASSERT, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add null String attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value: String? = null - - testedLogger.addAttribute(key, value) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - mapOf(key to NULL_MAP_VALUE), - emptySet() - ) - } - - @Test - fun `add Date attribute to logger`(forge: Forge, @Forgery value: Date) { - val key = forge.anAlphabeticalString() - - testedLogger.addAttribute(key, value) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add JsonObject attribute to logger`(forge: Forge, @Forgery value: JsonObject) { - val key = forge.anAlphabeticalString() - - testedLogger.addAttribute(key, value) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `add JsonArray attribute to logger`(forge: Forge, @Forgery value: JsonArray) { - val key = forge.anAlphabeticalString() - - testedLogger.addAttribute(key, value) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - // endregion - - // region removeAttribute - - @Test - fun `remove boolean attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aBool() - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.v(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.VERBOSE, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove int attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.anInt() - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.d(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.DEBUG, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove long attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aLong() - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove float attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aFloat() - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.w(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.WARN, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove double attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aDouble() - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.e(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.ERROR, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove null String attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value: String? = null - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.wtf(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.ASSERT, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove String attribute to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aNumericalString() - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove Date attribute to logger`(forge: Forge, @Forgery value: Date) { - val key = forge.anAlphabeticalString() - - testedLogger.addAttribute(key, value) - testedLogger.removeAttribute(key) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - // endregion - - // region Local Attributes - - @Test - fun `log message with local attributes`(forge: Forge) { - - val key = forge.anAlphabeticalString() - val value = forge.anInt() - - testedLogger.v(fakeMessage, null, mapOf(key to value)) - - verify(mockLogHandler) - .handleLog( - Log.VERBOSE, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `log message with local attributes and timestamp`(forge: Forge) { - val timestamp = forge.aLong() - val key = forge.anAlphabeticalString() - val value = forge.anInt() - val level = forge.anInt() - - testedLogger.internalLog(level, fakeMessage, null, mapOf(key to value), timestamp) - - verify(mockLogHandler) - .handleLog( - level, - fakeMessage, - null, - mapOf(key to value), - emptySet(), - timestamp - ) - } - - @Test - fun `log message with local attributes (null value)`(forge: Forge) { - - val key = forge.anAlphabeticalString() - val value: Any? = null - - testedLogger.d(fakeMessage, null, mapOf(key to value)) - - verify(mockLogHandler) - .handleLog( - Log.DEBUG, - fakeMessage, - null, - mapOf(key to value), - emptySet() - ) - } - - @Test - fun `log message with local attributes override logger value`(forge: Forge) { - - val key = forge.anAlphabeticalString() - val loggerValue = forge.aFloat() - val localValue = forge.anAlphabeticalString() - - testedLogger.addAttribute(key, loggerValue) - testedLogger.i(fakeMessage, null, mapOf(key to localValue)) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - mapOf(key to localValue), - emptySet() - ) - } - - @Test - fun `log message without local attributes after message with local attributes`(forge: Forge) { - - val key = forge.anAlphabeticalString() - val value = forge.anInt() - val message1 = forge.anAlphabeticalString() - val message2 = forge.anAlphabeticalString() - - testedLogger.w(message1, null, mapOf(key to value)) - testedLogger.e(message2) - - inOrder(mockLogHandler) { - verify(mockLogHandler) - .handleLog( - eq(Log.WARN), - eq(message1), - isNull(), - eq(mapOf(key to value)), - eq(emptySet()), - isNull() - ) - verify(mockLogHandler) - .handleLog( - eq(Log.ERROR), - eq(message2), - isNull(), - eq(emptyMap()), - eq(emptySet()), - isNull() - ) - } - } - - // endregion - - // region Tags - - @Test - fun `add simple tag to logger`(forge: Forge) { - val tag = forge.anAlphabeticalString() - - testedLogger.addTag(tag) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - setOf(tag) - ) - } - - @Test - fun `add key-value tag to logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.aNumericalString() - - testedLogger.addTag(key, value) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - setOf("$key:$value") - ) - } - - @Test - fun `add multiple tags with same key`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value1 = forge.anAlphabeticalString() - val value2 = forge.anAlphabeticalString() - val value3 = forge.anAlphabeticalString() - - testedLogger.addTag(key, value1) - testedLogger.addTag(key, value2) - testedLogger.addTag(key, value3) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - setOf("$key:$value1", "$key:$value2", "$key:$value3") - ) - } - - // endregion - - // region Remove Tags - - @Test - fun `remove tag from logger`(forge: Forge) { - val tag = forge.anAlphabeticalString() - - testedLogger.addTag(tag) - testedLogger.removeTag(tag) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove tag with key from logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value = forge.anAlphabeticalString() - - testedLogger.addTag(key, value) - testedLogger.removeTagsWithKey(key) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `remove all tags with key from logger`(forge: Forge) { - val key = forge.anAlphabeticalString() - val value1 = forge.anAlphabeticalString() - val value2 = forge.anAlphabeticalString() - val value3 = forge.anAlphabeticalString() - - testedLogger.addTag(key, value1) - testedLogger.addTag(key, value2) - testedLogger.addTag(key, value3) - testedLogger.removeTagsWithKey(key) - testedLogger.i(fakeMessage) - - verify(mockLogHandler) - .handleLog( - Log.INFO, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - // endregion - - // region Multi Thread Access - - @Test - fun `adding and removing tags is thread safe`(forge: Forge) { - val asyncOperations = 100 - val syncOperations = 10 - val randomTags = - forge.aList(asyncOperations) { - "${forge.aString(syncOperations)}:${forge.aString(syncOperations)}" - } - val countDownLatch = CountDownLatch(asyncOperations) - var logDebugExecutionCalls = 0 - repeat(asyncOperations) { - - val closure = when (forge.anInt(min = 0, max = 3)) { - 0 -> { - { - repeat(syncOperations) { - repeat(syncOperations) { - val randomTagIndex = forge.anInt(0, asyncOperations) - testedLogger.addTag(randomTags[randomTagIndex]) - } - } - } - } - 1 -> { - { - repeat(syncOperations) { - val randomTagIndex = forge.anInt(0, asyncOperations) - testedLogger.removeTag(randomTags[randomTagIndex]) - } - } - } - 2 -> { - { - repeat(syncOperations) { - val randomTagIndex = forge.anInt(0, asyncOperations) - val tagKey = randomTags[randomTagIndex].split(":").first() - testedLogger.removeTagsWithKey(tagKey) - } - } - } - 3 -> { - logDebugExecutionCalls++ - { - val attributes = - forge.aMap(size = forge.anInt(min = 1, max = 5)) { - forge.aString(size = syncOperations) to - forge.aString(size = syncOperations) - } - testedLogger.d( - forge.aString(size = syncOperations), - attributes = attributes - ) - } - } - else -> { - { } - } - } - async(countDownLatch, closure) - } - - countDownLatch.await(5, TimeUnit.SECONDS) - verify(mockLogHandler, times(logDebugExecutionCalls)).handleLog( - eq(Log.DEBUG), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) - } - - @Test - fun `adding and removing attributes is thread safe`(forge: Forge) { - val asyncOperations = 100 - val syncedOperations = 10 - val randomAttributes = forge.aList(size = asyncOperations) { - forge.aString(syncedOperations) to forge.aString(syncedOperations) - } - val countDownLatch = CountDownLatch(asyncOperations) - var logDebugExecutionCalls = 0 - repeat(asyncOperations) { - - val closure = when (forge.anInt(min = 0, max = 2)) { - 0 -> { - { - repeat(syncedOperations) { - val randomAttributeIndex = forge.anInt(0, asyncOperations) - testedLogger.addAttribute( - randomAttributes[randomAttributeIndex].first, - randomAttributes[randomAttributeIndex].second - ) - } - } - } - 1 -> { - { - repeat(syncedOperations) { - val randomAttributeIndex = forge.anInt(0, asyncOperations) - testedLogger.removeAttribute( - randomAttributes[randomAttributeIndex].first - ) - } - } - } - 2 -> { - logDebugExecutionCalls++ - { - val attributes = - forge.aMap(size = forge.anInt(min = 1, max = 5)) { - forge.aString(size = syncedOperations) to - forge.aString(size = syncedOperations) - } - testedLogger.d( - forge.aString(size = syncedOperations), - attributes = attributes - ) - } - } - else -> { - { } - } - } - async(countDownLatch, closure) - } - - countDownLatch.await(5, TimeUnit.SECONDS) - verify(mockLogHandler, times(logDebugExecutionCalls)).handleLog( - eq(Log.DEBUG), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) - } - - // endregion - - // region internal - - private fun async(countDownLatch: CountDownLatch, closure: () -> Unit) { - Thread { - closure() - countDownLatch.countDown() - }.start() - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/JsonObjectAssertExt.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/JsonObjectAssertExt.kt deleted file mode 100644 index 1929f86e92..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/JsonObjectAssertExt.kt +++ /dev/null @@ -1,40 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.assertj - -import com.datadog.android.core.internal.utils.NULL_MAP_VALUE -import com.datadog.android.core.internal.utils.toJsonArray -import com.datadog.tools.unit.assertj.JsonObjectAssert -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import java.util.Date - -fun JsonObjectAssert.containsExtraAttributes( - attributes: Map, - keyNamePrefix: String = "" -) { - attributes.filter { it.key.isNotBlank() } - .forEach { - val value = it.value - val key = keyNamePrefix + it.key - when (value) { - NULL_MAP_VALUE -> hasNullField(key) - null -> hasNullField(key) - is Boolean -> hasField(key, value) - is Int -> hasField(key, value) - is Long -> hasField(key, value) - is Float -> hasField(key, value) - is Double -> hasField(key, value) - is String -> hasField(key, value) - is Date -> hasField(key, value.time) - is JsonObject -> hasField(key, value) - is JsonArray -> hasField(key, value) - is Iterable<*> -> hasField(key, value.toJsonArray()) - else -> hasField(key, value.toString()) - } - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/LogAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/LogAssert.kt deleted file mode 100644 index 0c49b5ee6b..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/LogAssert.kt +++ /dev/null @@ -1,147 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.assertj - -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.user.UserInfo -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset - -internal class LogAssert(actual: Log) : - AbstractObjectAssert(actual, LogAssert::class.java) { - - fun hasLevel(expected: Int): LogAssert { - assertThat(actual.level) - .overridingErrorMessage( - "Expected log to have level $expected but was ${actual.level}" - ) - .isEqualTo(expected) - return this - } - - fun hasServiceName(expected: String): LogAssert { - assertThat(actual.serviceName) - .overridingErrorMessage( - "Expected log to have name $expected but was ${actual.serviceName}" - ) - .isEqualTo(expected) - return this - } - - fun hasMessage(expected: String): LogAssert { - assertThat(actual.message) - .overridingErrorMessage( - "Expected log to have message $expected but was ${actual.message}" - ) - .isEqualTo(expected) - return this - } - - fun hasThrowable(expected: Throwable?): LogAssert { - assertThat(actual.throwable) - .overridingErrorMessage( - "Expected log to have throwable $expected but was ${actual.throwable}" - ) - .isEqualTo(expected) - return this - } - - fun hasTimestampAround(expected: Long): LogAssert { - assertThat(actual.timestamp) - .overridingErrorMessage( - "Expected log to have timestamp $expected but was ${actual.timestamp}" - ) - .isCloseTo(expected, Offset.offset(200L)) - return this - } - - fun hasTimestamp(expected: Long): LogAssert { - assertThat(actual.timestamp) - .overridingErrorMessage( - "Expected log to have timestamp $expected but was ${actual.timestamp}" - ) - .isEqualTo(expected) - return this - } - - fun hasExactlyAttributes(attributes: Map): LogAssert { - assertThat(actual.attributes) - .hasSameSizeAs(attributes) - .containsAllEntriesOf(attributes) - return this - } - - fun hasExactlyTags(tags: Collection): LogAssert { - assertThat(actual.tags) - .containsExactlyInAnyOrder(*tags.toTypedArray()) - return this - } - - fun containsTags(tags: Collection): LogAssert { - assertThat(actual.tags) - .contains(*tags.toTypedArray()) - return this - } - - fun containsAttributes(attributes: Map): LogAssert { - assertThat(actual.attributes) - .containsAllEntriesOf(attributes) - return this - } - - fun hasNetworkInfo(expected: NetworkInfo?): LogAssert { - assertThat(actual.networkInfo) - .overridingErrorMessage( - "Expected log to have networkInfo $expected " + - "but was ${actual.networkInfo}" - ) - .isEqualTo(expected) - return this - } - - fun doesNotHaveNetworkInfo(): LogAssert { - assertThat(actual.networkInfo) - .overridingErrorMessage( - "Expected log to not have a networkInfo " + - "but instead it had ${actual.networkInfo}" - ) - .isNull() - return this - } - - fun hasUserInfo(expected: UserInfo?): LogAssert { - assertThat(actual.userInfo) - .isEqualTo(expected) - return this - } - - fun hasLoggerName(expected: String): LogAssert { - assertThat(actual.loggerName) - .overridingErrorMessage( - "Expected log to have loggerName $expected but was ${actual.loggerName}" - ) - .isEqualTo(expected) - return this - } - - fun hasThreadName(expected: String): LogAssert { - assertThat(actual.threadName) - .overridingErrorMessage( - "Expected log to have threadName $expected but was ${actual.threadName}" - ) - .isEqualTo(expected) - return this - } - - companion object { - - internal fun assertThat(actual: Log): LogAssert = - LogAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/SystemInfoAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/SystemInfoAssert.kt deleted file mode 100644 index eef6f0c96d..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/assertj/SystemInfoAssert.kt +++ /dev/null @@ -1,54 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.assertj - -import com.datadog.android.core.internal.system.SystemInfo -import kotlin.math.max -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset - -internal class SystemInfoAssert(actual: SystemInfo) : - AbstractObjectAssert(actual, SystemInfoAssert::class.java) { - - fun hasBatteryStatus(expected: SystemInfo.BatteryStatus): SystemInfoAssert { - assertThat(actual.batteryStatus) - .overridingErrorMessage( - "Expected systemInfo to have batteryStatus $expected " + - "but was ${actual.batteryStatus}" - ) - .isEqualTo(expected) - return this - } - - fun hasPowerSaveMode(expected: Boolean): SystemInfoAssert { - assertThat(actual.powerSaveMode) - .overridingErrorMessage( - "Expected systemInfo to have powerSaveMode $expected " + - "but was ${actual.powerSaveMode}" - ) - .isEqualTo(expected) - return this - } - - fun hasBatteryLevel(expected: Int, scale: Int = 100): SystemInfoAssert { - assertThat(actual.batteryLevel) - .overridingErrorMessage( - "Expected systemInfo to have batteryLevel $expected " + - "but was ${actual.batteryLevel}" - ) - .isCloseTo(expected, Offset.offset(max(100 / scale, 1))) - - return this - } - - companion object { - - internal fun assertThat(actual: SystemInfo): SystemInfoAssert = - SystemInfoAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/LogStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/LogStrategyTest.kt deleted file mode 100644 index d4a12a0380..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/LogStrategyTest.kt +++ /dev/null @@ -1,5 +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 2016-Present Datadog, Inc. - */ diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/LogsFeatureTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/LogsFeatureTest.kt deleted file mode 100644 index 1ec8a28b6a..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/LogsFeatureTest.kt +++ /dev/null @@ -1,372 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal - -import android.app.Application -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.data.upload.DataUploadScheduler -import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.log.internal.domain.LogFileStrategy -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.DatadogPluginConfig -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.net.URL -import java.util.concurrent.ExecutorService -import java.util.concurrent.ScheduledThreadPoolExecutor -import okhttp3.OkHttpClient -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class LogsFeatureTest { - - lateinit var mockAppContext: Application - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockSystemInfoProvider: SystemInfoProvider - - @Mock - lateinit var mockOkHttpClient: OkHttpClient - - @Mock - lateinit var mockScheduledThreadPoolExecutor: ScheduledThreadPoolExecutor - - @Mock - lateinit var mockPersistenceExecutorService: ExecutorService - - lateinit var trackingConsentProvider: ConsentProvider - - lateinit var fakeConfig: DatadogConfig.FeatureConfig - - lateinit var fakePackageName: String - lateinit var fakePackageVersion: String - - @TempDir - lateinit var tempRootDir: File - - @BeforeEach - fun `set up`(forge: Forge) { - trackingConsentProvider = TrackingConsentProvider() - CoreFeature.isMainProcess = true - fakeConfig = DatadogConfig.FeatureConfig( - clientToken = forge.anHexadecimalString(), - applicationId = forge.getForgery(), - endpointUrl = forge.getForgery().toString(), - envName = forge.anAlphabeticalString() - ) - - fakePackageName = forge.anAlphabeticalString() - fakePackageVersion = forge.aStringMatching("\\d(\\.\\d){3}") - - mockAppContext = mockContext(fakePackageName, fakePackageVersion) - whenever(mockAppContext.filesDir).thenReturn(tempRootDir) - whenever(mockAppContext.applicationContext) doReturn mockAppContext - } - - @AfterEach - fun `tear down`() { - LogsFeature.stop() - CoreFeature.stop() - } - - @Test - fun `initializes persistence strategy`() { - LogsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - - val persistenceStrategy = LogsFeature.persistenceStrategy - - assertThat(persistenceStrategy) - .isInstanceOf(LogFileStrategy::class.java) - } - - @Test - fun `initializes uploader thread`() { - LogsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - - val dataUploadScheduler = LogsFeature.dataUploadScheduler - - assertThat(dataUploadScheduler) - .isInstanceOf(DataUploadScheduler::class.java) - } - - @Test - fun `initializes from configuration`() { - LogsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - - val clientToken = LogsFeature.clientToken - val endpointUrl = LogsFeature.endpointUrl - - assertThat(clientToken).isEqualTo(fakeConfig.clientToken) - assertThat(endpointUrl).isEqualTo(fakeConfig.endpointUrl) - } - - @Test - fun `ignores if initialize called more than once`(forge: Forge) { - Datadog.setVerbosity(android.util.Log.VERBOSE) - LogsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - val persistenceStrategy = LogsFeature.persistenceStrategy - val dataUploadScheduler = LogsFeature.dataUploadScheduler - val clientToken = LogsFeature.clientToken - val endpointUrl = LogsFeature.endpointUrl - - fakeConfig = DatadogConfig.FeatureConfig( - clientToken = forge.anHexadecimalString(), - applicationId = forge.getForgery(), - endpointUrl = forge.getForgery().toString(), - envName = forge.anAlphabeticalString() - ) - LogsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - val persistenceStrategy2 = LogsFeature.persistenceStrategy - val dataUploadScheduler2 = LogsFeature.dataUploadScheduler - val clientToken2 = LogsFeature.clientToken - val endpointUrl2 = LogsFeature.endpointUrl - - assertThat(persistenceStrategy).isSameAs(persistenceStrategy2) - assertThat(dataUploadScheduler).isSameAs(dataUploadScheduler2) - assertThat(clientToken).isSameAs(clientToken2) - assertThat(endpointUrl).isSameAs(endpointUrl2) - } - - @Test - fun `M register the plugins as TrackingConsentProvideCallback W initialized`( - forge: Forge - ) { - // Given - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - val mockedTrackingConsentProvider: TrackingConsentProvider = mock() { - whenever(it.getConsent()).thenReturn(fakeConsent) - } - - // When - LogsFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockedTrackingConsentProvider - ) - // Then - val mockPlugins = plugins.toTypedArray() - mockPlugins.forEach { - verify(mockedTrackingConsentProvider).registerCallback(it) - } - } - - @Test - fun `it will register the provided plugin when feature was initialized`( - forge: Forge - ) { - // Given - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - val mockedTrackingConsentProvider: TrackingConsentProvider = mock() { - whenever(it.getConsent()).thenReturn(fakeConsent) - } - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - - // When - LogsFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockedTrackingConsentProvider - ) - - val argumentCaptor = argumentCaptor() - // Then - val mockPlugins = plugins.toTypedArray() - inOrder(*mockPlugins) { - mockPlugins.forEach { - verify(it).register(argumentCaptor.capture()) - } - } - - argumentCaptor.allValues.forEach { - assertThat(it).isInstanceOf(DatadogPluginConfig.LogsPluginConfig::class.java) - assertThat(it.context).isEqualTo(mockAppContext) - assertThat(it.serviceName).isEqualTo(CoreFeature.serviceName) - assertThat(it.envName).isEqualTo(fakeConfig.envName) - assertThat(it.featurePersistenceDirName).isEqualTo(LogFileStrategy.AUTHORIZED_FOLDER) - assertThat(it.context).isEqualTo(mockAppContext) - assertThat(it.trackingConsent).isEqualTo(fakeConsent) - } - } - - @Test - fun `it will unregister the provided plugin when stop called`( - forge: Forge - ) { - // Given - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - LogsFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - - // When - LogsFeature.stop() - - // Then - val mockPlugins = plugins.toTypedArray() - inOrder(*mockPlugins) { - mockPlugins.forEach { - verify(it).unregister() - } - } - } - - @Test - fun `will use a NoOpUploadScheduler if this is not the application main process`() { - // Given - CoreFeature.isMainProcess = false - - // When - LogsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - - // Then - assertThat(LogsFeature.dataUploadScheduler).isInstanceOf(NoOpUploadScheduler::class.java) - } - - @Test - fun `clears all files on local storage on request`( - @StringForgery(type = StringForgeryType.NUMERICAL) fileName: String, - @StringForgery content: String - ) { - val fakeDir = File(tempRootDir, LogFileStrategy.AUTHORIZED_FOLDER) - fakeDir.mkdirs() - val fakeFile = File(fakeDir, fileName) - fakeFile.writeText(content) - - // When - LogsFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - trackingConsentProvider - ) - LogsFeature.clearAllData() - - // Then - assertThat(fakeFile).doesNotExist() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogFileStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogFileStrategyTest.kt deleted file mode 100644 index 1e6ed10edf..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogFileStrategyTest.kt +++ /dev/null @@ -1,74 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import android.content.Context -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.assertj.PersistenceStrategyAssert -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.ExecutorService -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class LogFileStrategyTest { - lateinit var testedStrategy: LogFileStrategy - - lateinit var mockedContext: Context - - @Mock - lateinit var mockExecutorService: ExecutorService - - lateinit var trackingConsentProvider: TrackingConsentProvider - - @BeforeEach - fun `set up`() { - mockedContext = mockContext() - trackingConsentProvider = TrackingConsentProvider() - testedStrategy = LogFileStrategy( - mockedContext, - dataPersistenceExecutorService = mockExecutorService, - trackingConsentProvider = trackingConsentProvider - ) - } - - @Test - fun `M correctly initialise the strategy W instantiated`() { - val absolutePath = mockedContext.filesDir.absolutePath - val expectedIntermediateFolderPath = - absolutePath + - File.separator + - LogFileStrategy.INTERMEDIATE_DATA_FOLDER - val expectedAuthorizedFolderPath = - absolutePath + - File.separator + - LogFileStrategy.AUTHORIZED_FOLDER - PersistenceStrategyAssert - .assertThat(testedStrategy) - .hasIntermediateStorageFolder(expectedIntermediateFolderPath) - .hasAuthorizedStorageFolder(expectedAuthorizedFolderPath) - .uploadsFrom(expectedAuthorizedFolderPath) - .usesConsentAwareAsyncWriter() - .hasConfig(FilePersistenceConfig(LogFileStrategy.MAX_DELAY_BETWEEN_LOGS_MS)) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogGeneratorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogGeneratorTest.kt deleted file mode 100644 index dcfc732bae..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogGeneratorTest.kt +++ /dev/null @@ -1,572 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.assertj.LogAssert.Companion.assertThat -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.utils.forge.Configurator -import com.datadog.opentracing.DDSpanContext -import com.datadog.tools.unit.forge.aThrowable -import com.datadog.tools.unit.setStaticValue -import com.datadog.trace.api.interceptor.MutableSpan -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Span -import io.opentracing.Tracer -import io.opentracing.util.GlobalTracer -import java.util.UUID -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class LogGeneratorTest { - - lateinit var testedLogGenerator: LogGenerator - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Mock - lateinit var mockTracer: Tracer - - @Mock - lateinit var mockSpanContext: DDSpanContext - - @Mock(extraInterfaces = [MutableSpan::class]) - lateinit var mockSpan: Span - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - lateinit var fakeServiceName: String - lateinit var fakeLoggerName: String - lateinit var fakeAttributes: Map - lateinit var fakeTags: Set - lateinit var fakeAppVersion: String - lateinit var fakeEnvName: String - lateinit var fakeLogMessage: String - lateinit var fakeThrowable: Throwable - - @StringForgery(StringForgeryType.HEXADECIMAL) - lateinit var fakeSpanId: String - - @StringForgery(StringForgeryType.HEXADECIMAL) - lateinit var fakeTraceId: String - - @Forgery - lateinit var fakeNetworkInfo: NetworkInfo - - @Forgery - lateinit var fakeUserInfo: UserInfo - var fakeTimestamp = 0L - var fakeLevel: Int = 0 - lateinit var fakeAppId: String - lateinit var fakeSessionId: String - lateinit var fakeViewId: String - lateinit var fakeThreadName: String - - @BeforeEach - fun `set up`(forge: Forge) { - fakeServiceName = forge.anAlphabeticalString() - fakeLoggerName = forge.anAlphabeticalString() - fakeLogMessage = forge.anAlphabeticalString() - fakeLevel = forge.anInt(2, 8) - fakeAttributes = forge.aMap { anAlphabeticalString() to anInt() } - fakeTags = forge.aList { anAlphabeticalString() }.toSet() - fakeAppVersion = forge.aStringMatching("^[0-9]\\.[0-9]\\.[0-9]") - fakeEnvName = forge.aStringMatching("[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]") - fakeThrowable = forge.aThrowable() - fakeTimestamp = System.currentTimeMillis() - fakeAppId = UUID.randomUUID().toString() - fakeSessionId = UUID.randomUUID().toString() - fakeViewId = forge.anAlphabeticalString() - fakeThreadName = forge.anAlphabeticalString() - - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - whenever(mockTracer.activeSpan()).thenReturn(mockSpan) - whenever(mockSpan.context()) doReturn mockSpanContext - whenever(mockSpanContext.toSpanId()) doReturn fakeSpanId - whenever(mockSpanContext.toTraceId()) doReturn fakeTraceId - GlobalRum.updateRumContext(RumContext(fakeAppId, fakeSessionId, fakeViewId)) - GlobalRum.registerIfAbsent(mockRumMonitor) - GlobalTracer.registerIfAbsent(mockTracer) - testedLogGenerator = LogGenerator( - fakeServiceName, - fakeLoggerName, - mockNetworkInfoProvider, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ) - } - - @AfterEach - fun `tear down`() { - GlobalRum.isRegistered.set(false) - GlobalTracer::class.java.setStaticValue("isRegistered", false) - } - - @Test - fun `M add log message W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasMessage(fakeLogMessage) - } - - @Test - fun `M add the log level W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasLevel(fakeLevel) - } - - @Test - fun `M add the service name W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasServiceName(fakeServiceName) - } - - @Test - fun `M add the logger name W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasLoggerName(fakeLoggerName) - } - - @Test - fun `M add the thread name W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp, - fakeThreadName - ) - - // THEN - assertThat(log).hasThreadName(fakeThreadName) - } - - @Test - fun `M add the thread name as current thread W creating the Log {threadName not provided}`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasThreadName(Thread.currentThread().name) - } - - @Test - fun `M add the log timestamp W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasTimestamp(fakeTimestamp) - } - - @Test - fun `M add the throwable W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasThrowable(fakeThrowable) - } - - @Test - fun `M add the userNetworkInfo W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasUserInfo(fakeUserInfo) - } - - @Test - fun `M add the networkInfo W creating the Log`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).hasNetworkInfo(fakeNetworkInfo) - } - - @Test - fun `M not add the networkInfo W creating Log {networkInfoProvider is null}`() { - // GIVEN - testedLogGenerator = LogGenerator( - fakeServiceName, - fakeLoggerName, - null, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ) - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).doesNotHaveNetworkInfo() - } - - @Test - fun `M add the envNameTag W not empty`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).containsTags(listOf("${LogAttributes.ENV}:$fakeEnvName")) - } - - @Test - fun `M not add the envNameTag W empty`() { - // GIVEN - testedLogGenerator = LogGenerator( - fakeServiceName, - fakeLoggerName, - mockNetworkInfoProvider, - mockUserInfoProvider, - "", - fakeAppVersion - ) - - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - val expectedTags = fakeTags + "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - assertThat(log).hasExactlyTags(expectedTags) - } - - @Test - fun `M add the appVersionTag W not empty`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).containsTags(listOf("${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion")) - } - - @Test - fun `M not add the appVersionTag W not empty`() { - // GIVEN - testedLogGenerator = LogGenerator( - fakeServiceName, - fakeLoggerName, - mockNetworkInfoProvider, - mockUserInfoProvider, - fakeEnvName, - "" - ) - - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - val expectedTags = fakeTags + "${LogAttributes.ENV}:$fakeEnvName" - assertThat(log).hasExactlyTags(expectedTags) - } - - @Test - fun `M bundle the trace information W required`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).containsAttributes( - mapOf( - LogAttributes.DD_TRACE_ID to fakeTraceId, - LogAttributes.DD_SPAN_ID to fakeSpanId - ) - ) - } - - @Test - fun `M do nothing W required to bundle the trace information {no active Span}`() { - // GIVEN - whenever(mockTracer.activeSpan()).doReturn(null) - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - val expectedAttributes = fakeAttributes + mapOf( - LogAttributes.RUM_APPLICATION_ID to fakeAppId, - LogAttributes.RUM_SESSION_ID to fakeSessionId, - LogAttributes.RUM_VIEW_ID to fakeViewId - ) - assertThat(log).hasExactlyAttributes(expectedAttributes) - } - - @Test - fun `M do nothing W required to bundle the trace information {AndroidTracer not registered}`() { - // GIVEN - GlobalTracer::class.java.setStaticValue("isRegistered", false) - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - val expectedAttributes = fakeAttributes + mapOf( - LogAttributes.RUM_APPLICATION_ID to fakeAppId, - LogAttributes.RUM_SESSION_ID to fakeSessionId, - LogAttributes.RUM_VIEW_ID to fakeViewId - ) - assertThat(log).hasExactlyAttributes(expectedAttributes) - } - - @Test - fun `M do nothing W not required to bundle the trace information`() { - // GIVEN - GlobalTracer::class.java.setStaticValue("isRegistered", false) - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp, - bundleWithTraces = false - ) - - // THEN - val expectedAttributes = fakeAttributes + mapOf( - LogAttributes.RUM_APPLICATION_ID to fakeAppId, - LogAttributes.RUM_SESSION_ID to fakeSessionId, - LogAttributes.RUM_VIEW_ID to fakeViewId - ) - assertThat(log).hasExactlyAttributes(expectedAttributes) - } - - @Test - fun `M bundle the RUM information W required`() { - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - assertThat(log).containsAttributes( - mapOf( - LogAttributes.RUM_APPLICATION_ID to fakeAppId, - LogAttributes.RUM_SESSION_ID to fakeSessionId, - LogAttributes.RUM_VIEW_ID to fakeViewId - ) - ) - } - - @Test - fun `M do nothing W required to bundle the rum information {RumMonitor not registered}`() { - // GIVEN - GlobalRum.isRegistered.set(false) - - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp - ) - - // THEN - val expectedAttributes = fakeAttributes + mapOf( - LogAttributes.DD_TRACE_ID to fakeTraceId, - LogAttributes.DD_SPAN_ID to fakeSpanId - ) - assertThat(log).hasExactlyAttributes(expectedAttributes) - } - - @Test - fun `M do nothing W not required to bundle the rum information`() { - // GIVEN - GlobalRum.isRegistered.set(false) - - // WHEN - val log = testedLogGenerator.generateLog( - fakeLevel, - fakeLogMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - fakeTimestamp, - bundleWithRum = false - ) - - // THEN - val expectedAttributes = fakeAttributes + mapOf( - LogAttributes.DD_TRACE_ID to fakeTraceId, - LogAttributes.DD_SPAN_ID to fakeSpanId - ) - assertThat(log).hasExactlyAttributes(expectedAttributes) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogSerializerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogSerializerTest.kt deleted file mode 100644 index c01090dc74..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/domain/LogSerializerTest.kt +++ /dev/null @@ -1,319 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.domain - -import com.datadog.android.BuildConfig -import com.datadog.android.core.internal.constraints.DataConstraints -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.assertj.containsExtraAttributes -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.assertj.JsonObjectAssert.Companion.assertThat -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.google.gson.JsonPrimitive -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) -@ForgeConfiguration(Configurator::class) -internal class LogSerializerTest { - - lateinit var testedSerializer: LogSerializer - - @BeforeEach - fun `set up`() { - testedSerializer = LogSerializer() - } - - @Test - fun `serializes full log as json`(@Forgery fakeLog: Log) { - val serialized = testedSerializer.serialize(fakeLog) - assertSerializedLogMatchesInputLog(serialized, fakeLog) - } - - @Test - fun `serializes minimal log as json`(@Forgery fakeLog: Log) { - val minimalLog = fakeLog.copy( - throwable = null, - networkInfo = null, - userInfo = UserInfo(), - attributes = emptyMap(), - tags = emptyList() - ) - - val serialized = testedSerializer.serialize(minimalLog) - - assertSerializedLogMatchesInputLog(serialized, minimalLog) - } - - @Test - fun `ignores reserved attributes`(@Forgery fakeLog: Log, forge: Forge) { - // Given - val logWithoutAttributes = fakeLog.copy(attributes = emptyMap()) - val attributes = forge.aMap { - anElementFrom(*LogSerializer.reservedAttributes) to forge.anAsciiString() - }.toMap() - val logWithReservedAttributes = fakeLog.copy(attributes = attributes) - - // When - val serialized = testedSerializer.serialize(logWithReservedAttributes) - - // Then - assertSerializedLogMatchesInputLog(serialized, logWithoutAttributes) - } - - @Test - fun `ignores reserved tags keys`(@Forgery fakeLog: Log, forge: Forge) { - // Given - val logWithoutTags = fakeLog.copy(tags = emptyList()) - val key = forge.anElementFrom("host", "device", "source", "service") - val value = forge.aNumericalString() - val reservedTag = "$key:$value" - val logWithReservedTags = fakeLog.copy(tags = listOf(reservedTag)) - - // When - val serialized = testedSerializer.serialize(logWithReservedTags) - - // Then - assertSerializedLogMatchesInputLog(serialized, logWithoutTags) - } - - @Test - fun `serializes a log with no network info available`(@Forgery fakeLog: Log, forge: Forge) { - // Given - val logWithoutNetworkInfo = fakeLog.copy(networkInfo = null) - - // When - val serialized = testedSerializer.serialize(logWithoutNetworkInfo) - - // Then - assertSerializedLogMatchesInputLog(serialized, logWithoutNetworkInfo) - } - - @Test - fun `serializes a log with no throwable available`(@Forgery fakeLog: Log, forge: Forge) { - // Given - val logWithoutThrowable = fakeLog.copy(throwable = null) - - // When - val serialized = testedSerializer.serialize(logWithoutThrowable) - - // Then - assertSerializedLogMatchesInputLog(serialized, logWithoutThrowable) - } - - @Test - fun `M sanitise the user extra info keys W level deeper than 8`( - @Forgery fakeLog: Log, - forge: Forge - ) { - // GIVEN - val fakeBadKey = - forge.aList(size = 10) { forge.anAlphabeticalString() }.joinToString(".") - val lastDotIndex = fakeBadKey.lastIndexOf('.') - val expectedSanitisedKey = - fakeBadKey.replaceRange(lastDotIndex..lastDotIndex, "_") - val attributeValue = forge.anAlphabeticalString() - val fakeUserInfo = fakeLog.userInfo.copy(extraInfo = mapOf(fakeBadKey to attributeValue)) - - // WHEN - val serializedEvent = testedSerializer.serialize(fakeLog.copy(userInfo = fakeUserInfo)) - val jsonObject = JsonParser.parseString(serializedEvent).asJsonObject - - // THEN - assertThat(jsonObject) - .hasField( - "${LogAttributes.USR_ATTRIBUTES_GROUP}.$expectedSanitisedKey", - attributeValue - ) - assertThat(jsonObject) - .doesNotHaveField("${LogAttributes.USR_ATTRIBUTES_GROUP}.$fakeBadKey") - } - - @Test - fun `M use the attributes group verbose name W validateAttributes { user extra info }`( - @Forgery fakeLog: Log, - forge: Forge - ) { - // GIVEN - val mockedDataConstrains: DataConstraints = mock() - testedSerializer = LogSerializer(mockedDataConstrains) - - // WHEN - val serializedEvent = testedSerializer.serialize(fakeLog) - JsonParser.parseString(serializedEvent).asJsonObject - - // THEN - verify(mockedDataConstrains).validateAttributes( - any(), - eq(LogAttributes.USR_ATTRIBUTES_GROUP), - eq(LogSerializer.USER_EXTRA_GROUP_VERBOSE_NAME) - ) - } - - // region Internal - - private fun assertSerializedLogMatchesInputLog( - serializedObject: String, - log: Log - ) { - val jsonObject = JsonParser.parseString(serializedObject).asJsonObject - assertThat(jsonObject) - .hasField(LogAttributes.MESSAGE, log.message) - .hasField(LogAttributes.SERVICE_NAME, log.serviceName) - .hasField(LogAttributes.STATUS, levels[log.level]) - .hasField(LogAttributes.LOGGER_NAME, log.loggerName) - .hasField(LogAttributes.LOGGER_THREAD_NAME, log.threadName) - .hasField(LogAttributes.LOGGER_VERSION, BuildConfig.VERSION_NAME) - - // yyyy-mm-ddThh:mm:ss.SSSZ - assertThat(jsonObject) - .hasStringFieldMatching( - LogAttributes.DATE, - "\\d+\\-\\d{2}\\-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z" - ) - .containsExtraAttributes(log.attributes) - - assertJsonContainsNetworkInfo(jsonObject, log) - assertJsonContainsUserInfo(jsonObject, log) - assertJsonContainsCustomTags(jsonObject, log) - assertJsonContainsThrowableInfo(jsonObject, log) - } - - private fun assertJsonContainsNetworkInfo( - jsonObject: JsonObject, - log: Log - ) { - val info = log.networkInfo - if (info != null) { - assertThat(jsonObject).apply { - hasField(LogAttributes.NETWORK_CONNECTIVITY, info.connectivity.serialized) - if (!info.carrierName.isNullOrBlank()) { - hasField(LogAttributes.NETWORK_CARRIER_NAME, info.carrierName) - } else { - doesNotHaveField(LogAttributes.NETWORK_CARRIER_NAME) - } - if (info.carrierId >= 0) { - hasField(LogAttributes.NETWORK_CARRIER_ID, info.carrierId) - } else { - doesNotHaveField(LogAttributes.NETWORK_CARRIER_ID) - } - if (info.upKbps >= 0) { - hasField(LogAttributes.NETWORK_UP_KBPS, info.upKbps) - } else { - doesNotHaveField(LogAttributes.NETWORK_UP_KBPS) - } - if (info.downKbps >= 0) { - hasField(LogAttributes.NETWORK_DOWN_KBPS, info.downKbps) - } else { - doesNotHaveField(LogAttributes.NETWORK_DOWN_KBPS) - } - if (info.strength > Int.MIN_VALUE) { - hasField(LogAttributes.NETWORK_SIGNAL_STRENGTH, info.strength) - } else { - doesNotHaveField(LogAttributes.NETWORK_SIGNAL_STRENGTH) - } - } - } else { - assertThat(jsonObject) - .doesNotHaveField(LogAttributes.NETWORK_CONNECTIVITY) - .doesNotHaveField(LogAttributes.NETWORK_CARRIER_NAME) - .doesNotHaveField(LogAttributes.NETWORK_CARRIER_ID) - } - } - - private fun assertJsonContainsCustomTags( - jsonObject: JsonObject, - log: Log - ) { - val jsonTagString = (jsonObject[LogSerializer.TAG_DATADOG_TAGS] as? JsonPrimitive)?.asString - - if (jsonTagString.isNullOrBlank()) { - Assertions.assertThat(log.tags) - .isEmpty() - } else { - val tags = jsonTagString - .split(',') - .toList() - - Assertions.assertThat(tags) - .containsExactlyInAnyOrder(*log.tags.toTypedArray()) - } - } - - private fun assertJsonContainsThrowableInfo( - jsonObject: JsonObject, - log: Log - ) { - val throwable = log.throwable - if (throwable != null) { - assertThat(jsonObject) - .hasField(LogAttributes.ERROR_KIND, throwable.javaClass.simpleName) - .hasNullableField(LogAttributes.ERROR_MESSAGE, throwable.message) - .hasField(LogAttributes.ERROR_STACK, throwable.loggableStackTrace()) - } else { - assertThat(jsonObject) - .doesNotHaveField(LogAttributes.ERROR_KIND) - .doesNotHaveField(LogAttributes.ERROR_MESSAGE) - .doesNotHaveField(LogAttributes.ERROR_STACK) - } - } - - private fun assertJsonContainsUserInfo( - jsonObject: JsonObject, - log: Log - ) { - val info = log.userInfo - assertThat(jsonObject).apply { - if (info.id.isNullOrEmpty()) { - doesNotHaveField(LogAttributes.USR_ID) - } else { - hasField(LogAttributes.USR_ID, info.id) - } - if (info.name.isNullOrEmpty()) { - doesNotHaveField(LogAttributes.USR_NAME) - } else { - hasField(LogAttributes.USR_NAME, info.name) - } - if (info.email.isNullOrEmpty()) { - doesNotHaveField(LogAttributes.USR_EMAIL) - } else { - hasField(LogAttributes.USR_EMAIL, info.email) - } - containsExtraAttributes(info.extraInfo, LogAttributes.USR_ATTRIBUTES_GROUP + ".") - } - } - - // endregion - - companion object { - internal val levels = arrayOf( - "debug", "debug", "trace", "debug", "info", "warn", - "error", "critical", "debug", "emergency" - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/file/FileOrchestratorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/file/FileOrchestratorTest.kt deleted file mode 100644 index 0f51fa0be0..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/file/FileOrchestratorTest.kt +++ /dev/null @@ -1,404 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.file - -import com.datadog.android.core.internal.data.Orchestrator -import com.datadog.android.core.internal.data.file.FileOrchestrator -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import org.assertj.core.api.Assertions.assertThat -import org.junit.Assume.assumeTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings() -@ForgeConfiguration(Configurator::class) -internal class FileOrchestratorTest { - - @TempDir - lateinit var tempDir: File - lateinit var tempLogsDir: File - lateinit var testedOrchestrator: Orchestrator - - @StringForgery(type = StringForgeryType.ALPHABETICAL) - lateinit var fakeStorageFolderName: String - - @BeforeEach - fun `set up`() { - tempLogsDir = File(tempDir, fakeStorageFolderName) - tempLogsDir.mkdirs() - assumeTrue(tempLogsDir.exists() && tempLogsDir.isDirectory && tempLogsDir.canWrite()) - - testedOrchestrator = - FileOrchestrator( - tempLogsDir, - FilePersistenceConfig( - recentDelayMs = RECENT_DELAY_MS, - maxBatchSize = MAX_BATCH_SIZE, - maxItemsPerBatch = MAX_LOGS_PER_BATCH, - oldFileThreshold = OLD_FILE_THRESHOLD, - maxDiskSpace = MAX_DISK_SPACE - ) - ) - } - - @Test - fun `getWritableFile will returns null if the rootDirectory is a File`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int - ) { - // Given - val invalidRootDir = File(tempDir, "testPathName") - invalidRootDir.createNewFile() - - testedOrchestrator = - FileOrchestrator( - invalidRootDir, - FilePersistenceConfig( - recentDelayMs = RECENT_DELAY_MS, - maxBatchSize = MAX_BATCH_SIZE, - maxItemsPerBatch = MAX_LOGS_PER_BATCH, - oldFileThreshold = OLD_FILE_THRESHOLD, - maxDiskSpace = MAX_DISK_SPACE - ) - ) - // Then - assertThat(testedOrchestrator.getWritableFile(logSize)).isNull() - } - - @Test - fun `getReadableFile will return null if the rootDirectory is a File`() { - // Given - val invalidRootDir = File(tempDir, "testPathName") - invalidRootDir.createNewFile() - - testedOrchestrator = - FileOrchestrator( - invalidRootDir, - FilePersistenceConfig( - recentDelayMs = RECENT_DELAY_MS, - maxBatchSize = MAX_BATCH_SIZE, - maxItemsPerBatch = MAX_LOGS_PER_BATCH, - oldFileThreshold = OLD_FILE_THRESHOLD, - maxDiskSpace = MAX_DISK_SPACE - ) - ) - - // Then - assertThat(testedOrchestrator.getReadableFile(emptySet())).isNull() - } - - @Test - fun `getWritableFile returns null if the rootDirectory can't be created`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int - ) { - // Given - val corruptedRootDir = mock() - whenever(corruptedRootDir.mkdirs()).thenReturn(false) - - testedOrchestrator = - FileOrchestrator( - corruptedRootDir, - FilePersistenceConfig( - recentDelayMs = RECENT_DELAY_MS, - maxBatchSize = MAX_BATCH_SIZE, - maxItemsPerBatch = MAX_LOGS_PER_BATCH, - oldFileThreshold = OLD_FILE_THRESHOLD, - maxDiskSpace = MAX_DISK_SPACE - ) - ) - - // Then - assertThat(testedOrchestrator.getWritableFile(logSize)).isNull() - } - - @Test - fun `getReadableFile returns null the rootDirectory can't be created`() { - // Given - val corruptedRootDir = mock() - whenever(corruptedRootDir.mkdirs()).thenReturn(false) - - testedOrchestrator = - FileOrchestrator( - corruptedRootDir, - FilePersistenceConfig( - recentDelayMs = RECENT_DELAY_MS, - maxBatchSize = MAX_BATCH_SIZE, - maxItemsPerBatch = MAX_LOGS_PER_BATCH, - oldFileThreshold = OLD_FILE_THRESHOLD, - maxDiskSpace = MAX_DISK_SPACE - ) - ) - - // Then - assertThat(testedOrchestrator.getReadableFile(emptySet())).isNull() - } - - @Test - fun `reuse writeable file if possible`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize1: Int, - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize2: Int, - forge: Forge - ) { - val previousFile = testedOrchestrator.getWritableFile(logSize1) - previousFile?.createNewFile() - previousFile?.writeText(forge.anAsciiString(logSize1)) - - val writeableFile = testedOrchestrator.getWritableFile(logSize2) - - assertThat(writeableFile) - .isEqualTo(previousFile) - .exists() - } - - @Test - fun `create writeable file if none exists`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int - ) { - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .hasParent(tempLogsDir) - } - - @Test - fun `M recreate the rootDirectory W getWritableFile { rootDir deleted }`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int - ) { - // GIVEN - testedOrchestrator.getWritableFile(logSize) - tempLogsDir.deleteRecursively() - - // WHEN - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .hasParent(tempLogsDir) - } - - @Test - fun `create writeable file if previous exists but is unknown`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int - ) { - val now = System.currentTimeMillis() - 1000 - val previousFile = File(tempLogsDir, now.toString()) - previousFile.createNewFile() - - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .hasParent(tempLogsDir) - .isNotEqualTo(previousFile) - } - - @Test - fun `create writeable file if previous known has been deleted`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int - ) { - val previousFile = testedOrchestrator.getWritableFile(logSize) - Thread.sleep(10) - - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .hasParent(tempLogsDir) - .isNotEqualTo(previousFile) - } - - @Test - fun `create writeable file if previous known is too old`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int - ) { - val previousFile = testedOrchestrator.getWritableFile(logSize) - previousFile?.createNewFile() - Thread.sleep(RECENT_DELAY_MS) - - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .hasParent(tempLogsDir) - .isNotEqualTo(previousFile) - } - - @Test - fun `create writeable file if previous known is full in size`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int, - forge: Forge - ) { - val previousFile = testedOrchestrator.getWritableFile(logSize) - previousFile?.createNewFile() - previousFile?.writeText(forge.anAsciiString(MAX_BATCH_SIZE.toInt())) - - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .hasParent(tempLogsDir) - .isNotEqualTo(previousFile) - } - - @Test - fun `create writeable file if previous known is full in logs count`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int, - forge: Forge - ) { - val previousFile = testedOrchestrator.getWritableFile(logSize) - previousFile?.createNewFile() - previousFile?.writeText(forge.anAsciiString(logSize)) - for (i in 1 until MAX_LOGS_PER_BATCH) { - val f = testedOrchestrator.getWritableFile(logSize) - assumeTrue(f == previousFile) - f?.writeText(forge.anAsciiString(logSize)) - } - - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .hasParent(tempLogsDir) - .isNotEqualTo(previousFile) - } - - @Test - fun `getWritableFile discards oldest files if too many space is taken on disk`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize: Int, - forge: Forge - ) { - val batches = MAX_DISK_SPACE / MAX_BATCH_SIZE - val earlier = System.currentTimeMillis() - OLD_FILE_THRESHOLD - RECENT_DELAY_MS - val writtenFiles = mutableListOf() - for (i in 0..batches) { - val writtenFile = File(tempLogsDir, (earlier + i).toString()) - writtenFile.createNewFile() - writtenFile.writeText(forge.anAsciiString(MAX_BATCH_SIZE.toInt())) - writtenFiles.add(writtenFile) - } - - val writeableFile = testedOrchestrator.getWritableFile(logSize) - - assertThat(writeableFile) - .doesNotExist() - .isNotIn(writtenFiles) - assertThat(writtenFiles.first()) - .doesNotExist() - } - - @Test - fun `getReadableFile file`() { - val earlier = System.currentTimeMillis() - RECENT_DELAY_MS - RECENT_DELAY_MS - val writtenFile = File(tempLogsDir, earlier.toString()) - writtenFile.createNewFile() - - val readableFile = testedOrchestrator.getReadableFile(emptySet()) - - assertThat(readableFile) - .isEqualTo(writtenFile) - } - - @Test - fun `getReadableFile with excludes returns null`() { - val earlier = System.currentTimeMillis() - RECENT_DELAY_MS - RECENT_DELAY_MS - val writtenFile = File(tempLogsDir, earlier.toString()) - writtenFile.createNewFile() - - val readableFile = testedOrchestrator.getReadableFile(setOf(writtenFile.name)) - - assertThat(readableFile) - .isNull() - } - - @Test - fun `M return null W getReadableFile { rootDir deleted }`() { - // GIVEN - tempLogsDir.deleteRecursively() - - // THEN - assertThat(testedOrchestrator.getReadableFile(emptySet())).isNull() - } - - @Test - fun `getReadableFile ignores recent`(forge: Forge) { - val earlier = System.currentTimeMillis() - (RECENT_DELAY_MS / 2) - val writtenFile = File(tempLogsDir, earlier.toString()) - writtenFile.createNewFile() - writtenFile.writeText(forge.anAsciiString()) - - val readableFile = testedOrchestrator.getReadableFile(emptySet()) - - assertThat(readableFile) - .isNull() - } - - @Test - fun `getReadableFile discards obsolete files`(forge: Forge) { - val earlier = System.currentTimeMillis() - OLD_FILE_THRESHOLD - RECENT_DELAY_MS - val writtenFile = File(tempLogsDir, earlier.toString()) - writtenFile.createNewFile() - writtenFile.writeText(forge.anAsciiString()) - - val readableFile = testedOrchestrator.getReadableFile(emptySet()) - - assertThat(readableFile) - .isNull() - assertThat(writtenFile) - .doesNotExist() - } - - @Test - fun `M reset the internal attributes W reset`( - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize1: Int, - @IntForgery(min = 1, max = MAX_LOG_SIZE) logSize2: Int, - forge: Forge - ) { - // GIVEN - val previousFile = testedOrchestrator.getWritableFile(logSize1) - previousFile?.createNewFile() - previousFile?.writeText(forge.anAsciiString(logSize1)) - testedOrchestrator.getWritableFile(logSize2) - - // WHEN - testedOrchestrator.reset() - - // THEN - val testedFileOrchestrator = testedOrchestrator as FileOrchestrator - assertThat(testedFileOrchestrator.previousFileLogCount).isEqualTo(0) - assertThat(testedFileOrchestrator.previousFile).isNull() - } - - companion object { - private const val RECENT_DELAY_MS = 150L - - const val MAX_BATCH_SIZE: Long = 32 * 1024 - const val MAX_LOGS_PER_BATCH: Int = 32 - const val MAX_LOG_SIZE: Int = 256 - - const val OLD_FILE_THRESHOLD: Long = RECENT_DELAY_MS * 4 - const val MAX_DISK_SPACE = MAX_BATCH_SIZE * 4 - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/file/LogFileDataMigratorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/file/LogFileDataMigratorTest.kt deleted file mode 100644 index 4f32636846..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/file/LogFileDataMigratorTest.kt +++ /dev/null @@ -1,63 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.file - -import com.datadog.android.log.internal.domain.LogFileDataMigrator -import com.datadog.android.log.internal.domain.LogFileStrategy -import java.io.File -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings - -@Extensions( - ExtendWith(MockitoExtension::class) -) -@MockitoSettings() -internal class LogFileDataMigratorTest { - - lateinit var testedMigrator: LogFileDataMigrator - - @TempDir - lateinit var tempRootDir: File - - lateinit var tempOldDir: File - - @BeforeEach - fun `set up`() { - testedMigrator = - LogFileDataMigrator(tempRootDir) - tempOldDir = File(tempRootDir, LogFileStrategy.INTERMEDIATE_DATA_FOLDER) - tempOldDir.mkdirs() - } - - @Test - fun `will migrate all the data up to latest version`() { - testedMigrator.migrateData() - - assertThat(tempOldDir).doesNotExist() - } - - @Test - fun `will migrate data even if the old data directory does not exist`() { - tempOldDir.deleteRecursively() - - testedMigrator.migrateData() - - assertThat(tempOldDir).doesNotExist() - } - - @AfterEach - fun `tear down`() { - tempRootDir.deleteRecursively() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt deleted file mode 100644 index 53de2c7935..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt +++ /dev/null @@ -1,117 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class CombinedLogHandlerTest { - - lateinit var testedHandler: LogHandler - - lateinit var mockDevLogHandlers: Array - - lateinit var fakeServiceName: String - lateinit var fakeLoggerName: String - lateinit var fakeMessage: String - lateinit var fakeTags: Set - lateinit var fakeAttributes: Map - - var fakeLevel: Int = 0 - - @Forgery - lateinit var fakeThrowable: Throwable - - @BeforeEach - fun `set up`(forge: Forge) { - mockDevLogHandlers = forge.aList { mock() }.toTypedArray() - fakeServiceName = forge.anAlphabeticalString() - fakeLoggerName = forge.anAlphabeticalString() - fakeMessage = forge.anAlphabeticalString() - fakeLevel = forge.anInt(2, 8) - fakeAttributes = forge.aMap { anAlphabeticalString() to anInt() } - fakeTags = forge.aList { anAlphabeticalString() }.toSet() - - testedHandler = CombinedLogHandler(*mockDevLogHandlers) - } - - @Test - fun `forwards log`() { - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - mockDevLogHandlers.forEach { - verify(it).handleLog(fakeLevel, fakeMessage, fakeThrowable, fakeAttributes, fakeTags) - } - } - - @Test - fun `forwards log on background thread`(forge: Forge) { - val threadName = forge.anAlphabeticalString() - val countDownLatch = CountDownLatch(1) - val thread = Thread( - { - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - countDownLatch.countDown() - }, - threadName - ) - - thread.start() - countDownLatch.await(1, TimeUnit.SECONDS) - - mockDevLogHandlers.forEach { - verify(it).handleLog(fakeLevel, fakeMessage, fakeThrowable, fakeAttributes, fakeTags) - } - } - - @Test - fun `forwards minimal log`() { - testedHandler.handleLog( - fakeLevel, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - - mockDevLogHandlers.forEach { - verify(it).handleLog(fakeLevel, fakeMessage, null, emptyMap(), emptySet()) - } - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt deleted file mode 100644 index 6e96c1bf7d..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt +++ /dev/null @@ -1,194 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class ConditionalLogHandlerTest { - - lateinit var testedHandler: LogHandler - - @Mock - lateinit var mockDevLogHandler: LogHandler - - lateinit var fakeServiceName: String - lateinit var fakeLoggerName: String - lateinit var fakeMessage: String - lateinit var fakeTags: Set - lateinit var fakeAttributes: Map - - var fakeLevel: Int = 0 - - var fakeCondition = false - - @Forgery - lateinit var fakeThrowable: Throwable - - @BeforeEach - fun `set up`(forge: Forge) { - fakeServiceName = forge.anAlphabeticalString() - fakeLoggerName = forge.anAlphabeticalString() - fakeMessage = forge.anAlphabeticalString() - fakeLevel = forge.anInt(2, 8) - fakeAttributes = forge.aMap { anAlphabeticalString() to anInt() } - fakeTags = forge.aList { anAlphabeticalString() }.toSet() - - testedHandler = ConditionalLogHandler(mockDevLogHandler) { _, _ -> - fakeCondition - } - } - - @Test - fun `forwards log (condition true)`() { - fakeCondition = true - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - verify(mockDevLogHandler).handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - } - - @Test - fun `forwards log on background thread (condition true)`(forge: Forge) { - fakeCondition = true - val threadName = forge.anAlphabeticalString() - val countDownLatch = CountDownLatch(1) - val thread = Thread( - { - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - countDownLatch.countDown() - }, - threadName - ) - - thread.start() - countDownLatch.await(1, TimeUnit.SECONDS) - - verify(mockDevLogHandler).handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - } - - @Test - fun `forwards minimal log (condition true)`() { - fakeCondition = true - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - - verify(mockDevLogHandler).handleLog( - fakeLevel, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - } - - @Test - fun `forwards log (condition false)`() { - fakeCondition = false - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - verifyZeroInteractions(mockDevLogHandler) - } - - @Test - fun `forwards log on background thread (condition false)`(forge: Forge) { - fakeCondition = false - val threadName = forge.anAlphabeticalString() - val countDownLatch = CountDownLatch(1) - val thread = Thread( - { - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - countDownLatch.countDown() - }, - threadName - ) - - thread.start() - countDownLatch.await(1, TimeUnit.SECONDS) - - verifyZeroInteractions(mockDevLogHandler) - } - - @Test - fun `forwards minimal log (condition false)`() { - fakeCondition = false - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - - verifyZeroInteractions(mockDevLogHandler) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandlerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandlerTest.kt deleted file mode 100644 index 61b21b2734..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandlerTest.kt +++ /dev/null @@ -1,638 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -import android.util.Log as AndroidLog -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.sampling.Sampler -import com.datadog.android.log.LogAttributes -import com.datadog.android.log.assertj.LogAssert.Companion.assertThat -import com.datadog.android.log.internal.domain.Log -import com.datadog.android.log.internal.domain.LogGenerator -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.tracing.AndroidTracer -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.invokeMethod -import com.datadog.tools.unit.setFieldValue -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.noop.NoopTracerFactory -import io.opentracing.util.GlobalTracer -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogLogHandlerTest { - - lateinit var testedHandler: LogHandler - - lateinit var fakeServiceName: String - lateinit var fakeLoggerName: String - lateinit var fakeMessage: String - lateinit var fakeTags: Set - lateinit var fakeAttributes: Map - var fakeLevel: Int = 0 - - @Forgery - lateinit var fakeThrowable: Throwable - - @Forgery - lateinit var fakeNetworkInfo: NetworkInfo - - @Forgery - lateinit var fakeUserInfo: UserInfo - - @Mock - lateinit var mockWriter: Writer - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @Mock - lateinit var mockSampler: Sampler - - lateinit var fakeAppVersion: String - - lateinit var fakeEnvName: String - - @BeforeEach - fun `set up`(forge: Forge) { - fakeAppVersion = forge.aStringMatching("^[0-9]\\.[0-9]\\.[0-9]") - fakeEnvName = forge.aStringMatching("[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]") - fakeServiceName = forge.anAlphabeticalString() - fakeLoggerName = forge.anAlphabeticalString() - fakeMessage = forge.anAlphabeticalString() - fakeLevel = forge.anInt(2, 8) - fakeAttributes = forge.aMap { anAlphabeticalString() to anInt() } - fakeTags = forge.aList { anAlphabeticalString() }.toSet() - CoreFeature.envName = fakeEnvName - CoreFeature.packageVersion = fakeAppVersion - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - - testedHandler = DatadogLogHandler( - LogGenerator( - fakeServiceName, - fakeLoggerName, - mockNetworkInfoProvider, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ), - mockWriter - ) - } - - @AfterEach - fun `tear down`() { - GlobalTracer.get().setFieldValue("isRegistered", false) - GlobalTracer::class.java.setStaticValue("tracer", NoopTracerFactory.create()) - GlobalRum.isRegistered.set(false) - GlobalRum.monitor = NoOpRumMonitor() - } - - @Test - fun `forward log to LogWriter`() { - val now = System.currentTimeMillis() - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - null, - fakeAttributes, - fakeTags - ) - - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue) - .hasServiceName(fakeServiceName) - .hasLoggerName(fakeLoggerName) - .hasThreadName(Thread.currentThread().name) - .hasLevel(fakeLevel) - .hasMessage(fakeMessage) - .hasTimestampAround(now) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasExactlyAttributes(fakeAttributes) - .hasExactlyTags( - fakeTags + setOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - ) - ) - .hasThrowable(null) - } - } - - @Test - fun `forward log to LogWriter with throwable`() { - val now = System.currentTimeMillis() - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue) - .hasServiceName(fakeServiceName) - .hasLoggerName(fakeLoggerName) - .hasThreadName(Thread.currentThread().name) - .hasLevel(fakeLevel) - .hasMessage(fakeMessage) - .hasTimestampAround(now) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasExactlyAttributes(fakeAttributes) - .hasExactlyTags( - fakeTags + setOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - ) - ) - .hasThrowable(fakeThrowable) - } - } - - @Test - fun `doesn't forward low level log to RumMonitor`(forge: Forge) { - GlobalRum.registerIfAbsent(mockRumMonitor) - fakeLevel = forge.anInt(AndroidLog.VERBOSE, AndroidLog.ERROR) - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - verifyZeroInteractions(mockRumMonitor) - } - - @Test - fun `forward error log to RumMonitor`(forge: Forge) { - GlobalRum.registerIfAbsent(mockRumMonitor) - fakeLevel = forge.anElementFrom(AndroidLog.ERROR, AndroidLog.ASSERT) - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - null, - fakeAttributes, - fakeTags - ) - - verify(mockRumMonitor).addError( - fakeMessage, - RumErrorSource.LOGGER, - null, - fakeAttributes - ) - } - - @Test - fun `forward error log to RumMonitor with throwable`(forge: Forge) { - GlobalRum.registerIfAbsent(mockRumMonitor) - fakeLevel = forge.anElementFrom(AndroidLog.ERROR, AndroidLog.ASSERT) - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - verify(mockRumMonitor).addError( - fakeMessage, - RumErrorSource.LOGGER, - fakeThrowable, - fakeAttributes - ) - } - - @Test - fun `forward log with custom timestamp to LogWriter`(forge: Forge) { - val customTimestamp = forge.aLong() - - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags, - customTimestamp - ) - - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue) - .hasServiceName(fakeServiceName) - .hasLoggerName(fakeLoggerName) - .hasThreadName(Thread.currentThread().name) - .hasLevel(fakeLevel) - .hasMessage(fakeMessage) - .hasTimestampAround(customTimestamp) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasExactlyAttributes(fakeAttributes) - .hasExactlyTags( - fakeTags + setOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - ) - ) - } - } - - @Test - fun `forward log to LogWriter on background thread`(forge: Forge) { - val now = System.currentTimeMillis() - val threadName = forge.anAlphabeticalString() - val countDownLatch = CountDownLatch(1) - val thread = Thread( - { - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - countDownLatch.countDown() - }, - threadName - ) - - thread.start() - countDownLatch.await(1, TimeUnit.SECONDS) - - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue) - .hasServiceName(fakeServiceName) - .hasLoggerName(fakeLoggerName) - .hasThreadName(threadName) - .hasLevel(fakeLevel) - .hasMessage(fakeMessage) - .hasTimestampAround(now) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasExactlyAttributes(fakeAttributes) - .hasExactlyTags( - fakeTags + setOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - ) - ) - } - } - - @Test - fun `forward log to LogWriter without network info`() { - val now = System.currentTimeMillis() - testedHandler = DatadogLogHandler( - LogGenerator( - fakeServiceName, - fakeLoggerName, - null, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ), - mockWriter - ) - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue) - .hasServiceName(fakeServiceName) - .hasLoggerName(fakeLoggerName) - .hasThreadName(Thread.currentThread().name) - .hasLevel(fakeLevel) - .hasMessage(fakeMessage) - .hasTimestampAround(now) - .hasNetworkInfo(null) - .hasUserInfo(fakeUserInfo) - .hasExactlyAttributes(fakeAttributes) - .hasExactlyTags( - fakeTags + setOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - ) - ) - } - } - - @Test - fun `forward minimal log to LogWriter`() { - val now = System.currentTimeMillis() - GlobalRum.isRegistered.set(false) - testedHandler = DatadogLogHandler( - LogGenerator( - fakeServiceName, - fakeLoggerName, - null, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ), - mockWriter - ) - testedHandler.handleLog( - fakeLevel, - fakeMessage, - null, - emptyMap(), - emptySet() - ) - - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue) - .hasServiceName(fakeServiceName) - .hasLoggerName(fakeLoggerName) - .hasThreadName(Thread.currentThread().name) - .hasLevel(fakeLevel) - .hasMessage(fakeMessage) - .hasTimestampAround(now) - .hasNetworkInfo(null) - .hasUserInfo(fakeUserInfo) - .hasExactlyAttributes(emptyMap()) - .hasExactlyTags( - setOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - ) - ) - .hasThrowable(null) - } - } - - @Test - fun `it will add the span id and trace id if we active an active tracer`(forge: Forge) { - // Given - val config = - DatadogConfig.Builder(forge.anAlphabeticalString(), forge.anAlphabeticalString()) - .build() - Datadog.initialize(mockContext(), config) - val tracer = AndroidTracer.Builder().build() - val span = tracer.buildSpan(forge.anAlphabeticalString()).start() - tracer.activateSpan(span) - GlobalTracer.registerIfAbsent(tracer) - - // When - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - // Then - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue.attributes) - .containsEntry(LogAttributes.DD_TRACE_ID, tracer.traceId) - .containsEntry(LogAttributes.DD_SPAN_ID, tracer.spanId) - } - Datadog.invokeMethod("stop") - } - - @Test - fun `it will not add trace deps if we do not have active an active tracer`(forge: Forge) { - // When - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - // Then - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue.attributes) - .doesNotContainKey(LogAttributes.DD_TRACE_ID) - .doesNotContainKey(LogAttributes.DD_SPAN_ID) - } - } - - @Test - fun `it will add the Rum context`(forge: Forge) { - // Given - val config = - DatadogConfig.Builder(forge.anAlphabeticalString(), forge.anAlphabeticalString()) - .build() - Datadog.initialize(mockContext(), config) - val rumContext = forge.getForgery() - GlobalRum.updateRumContext(rumContext) - GlobalRum.registerIfAbsent(mockRumMonitor) - - // When - testedHandler.handleLog( - fakeLevel, - fakeMessage, - null, - fakeAttributes, - fakeTags - ) - - // Then - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue.attributes) - .containsEntry( - LogAttributes.RUM_APPLICATION_ID, - rumContext.applicationId - ) - .containsEntry(LogAttributes.RUM_SESSION_ID, rumContext.sessionId) - .containsEntry(LogAttributes.RUM_VIEW_ID, rumContext.viewId) - } - Datadog.invokeMethod("stop") - } - - @Test - fun `it will not add trace deps if the flag was set to false`(forge: Forge) { - // Given - testedHandler = DatadogLogHandler( - LogGenerator( - fakeServiceName, - fakeLoggerName, - mockNetworkInfoProvider, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ), - mockWriter, - bundleWithTraces = false - ) - // When - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - // Then - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue.attributes) - .doesNotContainKey(LogAttributes.DD_TRACE_ID) - .doesNotContainKey(LogAttributes.DD_SPAN_ID) - } - } - - @Test - fun `it will sample out the logs when required`() { - // Given - whenever(mockSampler.sample()).thenReturn(false) - testedHandler = DatadogLogHandler( - LogGenerator( - fakeServiceName, - fakeLoggerName, - mockNetworkInfoProvider, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ), - mockWriter, - bundleWithTraces = false, - sampler = mockSampler - ) - - // When - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - // Then - verifyZeroInteractions(mockWriter) - } - - @Test - fun `it will sample in the logs when required`() { - // Given - val now = System.currentTimeMillis() - whenever(mockSampler.sample()).thenReturn(true) - testedHandler = DatadogLogHandler( - LogGenerator( - fakeServiceName, - fakeLoggerName, - mockNetworkInfoProvider, - mockUserInfoProvider, - fakeEnvName, - fakeAppVersion - ), - mockWriter, - bundleWithTraces = false, - sampler = mockSampler - ) - - // When - testedHandler.handleLog( - fakeLevel, - fakeMessage, - fakeThrowable, - fakeAttributes, - fakeTags - ) - - // Then - argumentCaptor().apply { - verify(mockWriter).write(capture()) - - assertThat(lastValue) - .hasServiceName(fakeServiceName) - .hasLoggerName(fakeLoggerName) - .hasLevel(fakeLevel) - .hasMessage(fakeMessage) - .hasTimestampAround(now) - .hasNetworkInfo(fakeNetworkInfo) - .hasUserInfo(fakeUserInfo) - .hasExactlyAttributes(fakeAttributes) - .hasExactlyTags( - fakeTags + setOf( - "${LogAttributes.ENV}:$fakeEnvName", - "${LogAttributes.APPLICATION_VERSION}:$fakeAppVersion" - ) - ) - } - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandlerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandlerTest.kt deleted file mode 100644 index 0ae64be270..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandlerTest.kt +++ /dev/null @@ -1,87 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.logger - -import com.datadog.android.BuildConfig -import com.datadog.android.Datadog -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(ForgeExtension::class) -internal class LogcatLogHandlerTest { - - lateinit var testedHandler: LogcatLogHandler - - @StringForgery - lateinit var fakeServiceName: String - - @BeforeEach - fun `set up`() { - testedHandler = LogcatLogHandler(fakeServiceName, true) - } - - @AfterEach - fun `tear down`() { - Datadog.isDebug = BuildConfig.DEBUG - } - - @Test - fun `resolves stack trace element null if in release mode`() { - Datadog.isDebug = false - - val element = testedHandler.getCallerStackElement() - - assertThat(element) - .isNull() - } - - @Test - fun `resolves stack trace element null if useClassnameAsTag=false`() { - testedHandler = LogcatLogHandler(fakeServiceName, false) - Datadog.isDebug = true - - val element = testedHandler.getCallerStackElement() - - assertThat(element) - .isNull() - } - - @Test - fun `resolves stack trace element from caller`() { - Datadog.isDebug = true - - val element = testedHandler.getCallerStackElement() - - checkNotNull(element) - assertThat(element.className) - .isEqualTo(javaClass.canonicalName) - } - - @Test - fun `resolves nested stack trace element from caller`() { - Datadog.isDebug = true - - var element: StackTraceElement? = null - - val runnable = Runnable { - element = testedHandler.getCallerStackElement() - } - runnable.run() - - assertThat(element!!.className) - .isEqualTo( - "${javaClass.canonicalName}" + - "\$resolves nested stack trace element from caller" + - "\$runnable\$1" - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/user/DatadogUserInfoProviderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/user/DatadogUserInfoProviderTest.kt deleted file mode 100644 index 04663f13e0..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/internal/user/DatadogUserInfoProviderTest.kt +++ /dev/null @@ -1,49 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.internal.user - -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(ForgeExtension::class) -@ForgeConfiguration(Configurator::class) -internal class DatadogUserInfoProviderTest { - - lateinit var testedProvider: DatadogUserInfoProvider - - @BeforeEach - fun `set up`() { - testedProvider = DatadogUserInfoProvider() - } - - @Test - fun `𝕄 return default userInfo 𝕎 getUserInfo()`() { - // When - val result = testedProvider.getUserInfo() - - // Then - assertThat(result).isEqualTo(UserInfo()) - } - - @Test - fun `𝕄 return saved userInfo 𝕎 setUserInfo() and getUserInfo()`( - @Forgery userInfo: UserInfo - ) { - // When - testedProvider.setUserInfo(userInfo) - val result = testedProvider.getUserInfo() - - // Then - assertThat(result).isEqualTo(userInfo) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/net/LogsOkHttpUploaderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/log/net/LogsOkHttpUploaderTest.kt deleted file mode 100644 index 8f7f8af8c5..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/log/net/LogsOkHttpUploaderTest.kt +++ /dev/null @@ -1,53 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.log.net - -import com.datadog.android.core.internal.net.DataOkHttpUploader -import com.datadog.android.core.internal.net.DataOkHttpUploaderTest -import com.datadog.android.log.internal.net.LogsOkHttpUploader -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.TimeUnit -import okhttp3.OkHttpClient -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class LogsOkHttpUploaderTest : DataOkHttpUploaderTest() { - - override fun uploader(): LogsOkHttpUploader { - return LogsOkHttpUploader( - fakeEndpoint, - fakeToken, - OkHttpClient.Builder() - .connectTimeout(TIMEOUT_TEST_MS, TimeUnit.MILLISECONDS) - .readTimeout(TIMEOUT_TEST_MS, TimeUnit.MILLISECONDS) - .writeTimeout(TIMEOUT_TEST_MS, TimeUnit.MILLISECONDS) - .build() - ) - } - - override fun urlFormat(): String { - return LogsOkHttpUploader.UPLOAD_URL - } - - override fun expectedPathRegex(): String { - return "^\\/v1\\/input/$fakeToken" + - "\\?${DataOkHttpUploader.QP_BATCH_TIME}=\\d+" + - "&${DataOkHttpUploader.QP_SOURCE}=${DataOkHttpUploader.DD_SOURCE_ANDROID}" + - "$" - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/ActivityLifecycleTrackingStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/ActivityLifecycleTrackingStrategyTest.kt deleted file mode 100644 index 6898cb0b04..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/ActivityLifecycleTrackingStrategyTest.kt +++ /dev/null @@ -1,109 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import android.app.Activity -import android.app.Application -import android.content.Context -import android.content.Intent -import android.view.Window -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.tracking.ActivityLifecycleTrackingStrategy -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal abstract class ActivityLifecycleTrackingStrategyTest { - - lateinit var testedStrategy: ActivityLifecycleTrackingStrategy - - @Mock - lateinit var mockIntent: Intent - - @Mock - lateinit var mockRumMonitor: AdvancedRumMonitor - - @Mock - lateinit var mockActivity: Activity - - @Mock - lateinit var mockWindow: Window - - @Mock - lateinit var mockAppContext: Application - - @Mock - lateinit var mockBadContext: Context - - @BeforeEach - open fun `set up`(forge: Forge) { - GlobalRum.registerIfAbsent(mockRumMonitor) - whenever(mockActivity.intent).thenReturn(mockIntent) - whenever(mockActivity.window).thenReturn(mockWindow) - } - - @AfterEach - open fun `tear down`() { - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.isRegistered.set(false) - } - - @Test - fun `when register it will register as lifecycle callback`() { - // When - testedStrategy.register(mockAppContext) - - // verify - verify(mockAppContext).registerActivityLifecycleCallbacks(testedStrategy) - } - - @Test - fun `when unregister it will remove itself as lifecycle callback`() { - // When - testedStrategy.unregister(mockAppContext) - - // verify - verify(mockAppContext).unregisterActivityLifecycleCallbacks(testedStrategy) - } - - @Test - fun `when register called with non application context will do nothing`() { - // When - testedStrategy.register(mockBadContext) - - // verify - verifyZeroInteractions(mockBadContext) - } - - @Test - fun `when unregister called with non application context will do nothing`() { - // When - testedStrategy.unregister(mockBadContext) - - // verify - verifyZeroInteractions(mockBadContext) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/ActivityViewTrackingStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/ActivityViewTrackingStrategyTest.kt deleted file mode 100644 index 2fb20c8b03..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/ActivityViewTrackingStrategyTest.kt +++ /dev/null @@ -1,321 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import android.app.Activity -import android.os.Bundle -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.tracking.ViewLoadingTimer -import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy -import com.datadog.android.rum.tracking.ComponentPredicate -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.setFieldValue -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class ActivityViewTrackingStrategyTest : ActivityLifecycleTrackingStrategyTest() { - - @Mock - lateinit var mockViewLoadingTimer: ViewLoadingTimer - - // region tests - - @BeforeEach - override fun `set up`(forge: Forge) { - super.`set up`(forge) - testedStrategy = - ActivityViewTrackingStrategy(true) - testedStrategy.setFieldValue("viewLoadingTimer", mockViewLoadingTimer) - } - - @Test - fun `when created will notify the viewLoadingTimer`(forge: Forge) { - // When - testedStrategy.onActivityCreated(mockActivity, null) - // Then - verify(mockViewLoadingTimer).onCreated(mockActivity) - } - - @Test - fun `when created will do nothing if activity not whitelisted`(forge: Forge) { - // Given - testedStrategy = ActivityViewTrackingStrategy( - true, - componentPredicate = object : - ComponentPredicate { - override fun accept(component: Activity): Boolean { - return false - } - } - ) - // When - testedStrategy.onActivityCreated(mockActivity, null) - // Then - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when started will notify the viewLoadingTimer for startLoading`() { - // Whenever - testedStrategy.onActivityStarted(mockActivity) - - // Then - verify(mockViewLoadingTimer).onStartLoading(mockActivity) - } - - @Test - fun `when started and activity not whitelisted will do nothing`() { - // Given - testedStrategy = ActivityViewTrackingStrategy( - trackExtras = false, - componentPredicate = object : - ComponentPredicate { - override fun accept(component: Activity): Boolean { - return false - } - } - ) - - // Whenever - testedStrategy.onActivityStarted(mockActivity) - - // Then - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when resumed it will start a view event`(forge: Forge) { - // When - testedStrategy.onActivityResumed(mockActivity) - // Then - verify(mockRumMonitor).startView( - eq(mockActivity), - eq(mockActivity.resolveViewName()), - eq(emptyMap()) - ) - } - - @Test - fun `when resumed will start a view event with intent extras as attributes`( - forge: Forge - ) { - // Given - val arguments = Bundle() - val expectedAttrs = mutableMapOf() - for (i in 0..10) { - val key = forge.anAlphabeticalString() - val value = forge.anAsciiString() - arguments.putString(key, value) - expectedAttrs["view.arguments.$key"] = value - } - whenever(mockIntent.extras).thenReturn(arguments) - whenever(mockActivity.intent).thenReturn(mockIntent) - - // Whenever - testedStrategy.onActivityResumed(mockActivity) - - verify(mockRumMonitor).startView( - eq(mockActivity), - eq(mockActivity.resolveViewName()), - eq(expectedAttrs) - ) - } - - @Test - fun `when resumed and not tracking intent extras will send empty attributes`( - forge: Forge - ) { - // Given - testedStrategy = - ActivityViewTrackingStrategy(false) - val arguments = Bundle() - for (i in 0..10) { - val key = forge.anAlphabeticalString() - val value = forge.anAsciiString() - arguments.putString(key, value) - } - whenever(mockIntent.extras).thenReturn(arguments) - whenever(mockActivity.intent).thenReturn(mockIntent) - - // Whenever - testedStrategy.onActivityResumed(mockActivity) - - verify(mockRumMonitor).startView( - eq(mockActivity), - eq(mockActivity.resolveViewName()), - eq(emptyMap()) - ) - } - - @Test - fun `when resumed will do nothing if activity is not whitelisted`() { - // Given - testedStrategy = ActivityViewTrackingStrategy( - trackExtras = false, - componentPredicate = object : - ComponentPredicate { - override fun accept(component: Activity): Boolean { - return false - } - } - ) - - // Whenever - testedStrategy.onActivityResumed(mockActivity) - - // Then - verifyZeroInteractions(mockRumMonitor) - } - - @Test - fun `when postResumed will notify the viewLoadingTimer for stopLoading`() { - // Whenever - testedStrategy.onActivityPostResumed(mockActivity) - - // Then - verify(mockViewLoadingTimer).onFinishedLoading(mockActivity) - } - - @Test - fun `when resumed will notify the viewLoadingTimer for stopLoading`() { - // Whenever - testedStrategy.onActivityResumed(mockActivity) - - // Then - verify(mockViewLoadingTimer).onFinishedLoading(mockActivity) - } - - @Test - fun `when postResumed and activity not whitelisted will do nothing`() { - // Given - testedStrategy = ActivityViewTrackingStrategy( - trackExtras = false, - componentPredicate = object : - ComponentPredicate { - override fun accept(component: Activity): Boolean { - return false - } - } - ) - - // Whenever - testedStrategy.onActivityPostResumed(mockActivity) - - // Then - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when paused it will update the view loading time and stop it in this order`(forge: Forge) { - // Given - val expectedLoadingTime = forge.aLong() - val firsTimeLoading = forge.aBool() - val expectedLoadingType = - if (firsTimeLoading) { - ViewEvent.LoadingType.ACTIVITY_DISPLAY - } else { - ViewEvent.LoadingType.ACTIVITY_REDISPLAY - } - whenever(mockViewLoadingTimer.getLoadingTime(mockActivity)) - .thenReturn(expectedLoadingTime) - whenever(mockViewLoadingTimer.isFirstTimeLoading(mockActivity)) - .thenReturn(firsTimeLoading) - - // When - testedStrategy.onActivityPaused(mockActivity) - - // Then - inOrder(mockRumMonitor, mockViewLoadingTimer) { - verify(mockRumMonitor).updateViewLoadingTime( - mockActivity, - expectedLoadingTime, - expectedLoadingType - ) - verify(mockRumMonitor).stopView(mockActivity, emptyMap()) - verify(mockViewLoadingTimer).onPaused(mockActivity) - } - } - - @Test - fun `when paused will do nothing if activity is not whitelisted`() { - // Given - testedStrategy = ActivityViewTrackingStrategy( - trackExtras = false, - componentPredicate = object : - ComponentPredicate { - override fun accept(component: Activity): Boolean { - return false - } - } - ) - - // Whenever - testedStrategy.onActivityPaused(mockActivity) - - // Then - verifyZeroInteractions(mockRumMonitor) - } - - @Test - fun `when activity destroyed will notify the viewLoadingTimer for onDestroy`() { - // Whenever - testedStrategy.onActivityDestroyed(mockActivity) - - // Then - verify(mockViewLoadingTimer).onDestroyed(mockActivity) - } - - @Test - fun `when activity destroyed and not whitelisted will do nothing`() { - // Given - testedStrategy = ActivityViewTrackingStrategy( - trackExtras = false, - componentPredicate = object : - ComponentPredicate { - override fun accept(component: Activity): Boolean { - return false - } - } - ) - - // Whenever - testedStrategy.onActivityDestroyed(mockActivity) - - // Then - verifyZeroInteractions(mockViewLoadingTimer) - } - - // endregion - - // region internal - - private fun Any.resolveViewName(): String { - return javaClass.canonicalName ?: javaClass.simpleName - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/GlobalRumTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/GlobalRumTest.kt deleted file mode 100644 index 82ff58e12c..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/GlobalRumTest.kt +++ /dev/null @@ -1,273 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import com.datadog.android.error.internal.CrashReportsFeature -import com.datadog.android.log.internal.LogsFeature -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.tracing.internal.TracesFeature -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.invokeMethod -import com.datadog.tools.unit.setFieldValue -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.lang.Exception -import java.util.concurrent.CountDownLatch -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class GlobalRumTest { - - @BeforeEach - fun `set up`() { - GlobalRum.isRegistered.set(false) - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.sessionStartNs.set(0L) - } - - @AfterEach - fun `tear down`() { - GlobalRum.isRegistered.set(false) - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.updateRumContext(RumContext()) - GlobalRum.sessionStartNs.set(0L) - } - - @Test - fun `M register monitor W registerIfAbsent()`() { - val monitor: RumMonitor = mock() - - GlobalRum.registerIfAbsent(monitor) - - assertThat(GlobalRum.get()) - .isSameAs(monitor) - } - - @Test - fun `M register monitor only once W registerIfAbsent() twice`() { - val monitor: RumMonitor = mock() - val monitor2: RumMonitor = mock() - - GlobalRum.registerIfAbsent(monitor) - GlobalRum.registerIfAbsent(monitor2) - - assertThat(GlobalRum.get()) - .isSameAs(monitor) - } - - @Test - fun `M add global attributes W addAttribute()`( - @StringForgery key: String, - @StringForgery(type = StringForgeryType.ASCII) value: String - ) { - GlobalRum.addAttribute(key, value) - - assertThat(GlobalRum.globalAttributes) - .containsEntry(key, value) - } - - @Test - fun `M overwrite global attributes W addAttribute() twice {same key different value}`( - @StringForgery key: String, - @StringForgery(type = StringForgeryType.ASCII) value: String, - @StringForgery(type = StringForgeryType.ASCII) value2: String - ) { - GlobalRum.addAttribute(key, value) - GlobalRum.addAttribute(key, value2) - - assertThat(GlobalRum.globalAttributes) - .containsEntry(key, value2) - } - - @Test - fun `M remove global attributes W addAttribute() and removeAttribute()`( - @StringForgery key: String, - @StringForgery(type = StringForgeryType.ASCII) value: String - ) { - GlobalRum.addAttribute(key, value) - assertThat(GlobalRum.globalAttributes) - .containsEntry(key, value) - - GlobalRum.removeAttribute(key) - assertThat(GlobalRum.globalAttributes) - .doesNotContainKey(key) - } - - @Test - fun `M add global attributes W addAttribute() {multithreaded}`( - @StringForgery key: String - ) { - var errors = 0 - val countDownLatch = CountDownLatch(2) - val threadAdd = Thread { - try { - for (i in 0..128) { - GlobalRum.addAttribute("$key$i", "value-$i") - } - } catch (e: Exception) { - errors++ - } finally { - countDownLatch.countDown() - } - } - - val threadRead = Thread { - try { - for (i in 0..128) { - val iterator = GlobalRum.globalAttributes.iterator() - while (iterator.hasNext()) { - val entry = iterator.next() - println("${entry.key} = ${entry.value}") - } - } - } catch (e: Exception) { - errors++ - } finally { - countDownLatch.countDown() - } - } - - threadRead.start() - threadAdd.start() - - countDownLatch.await() - - assertThat(errors).isEqualTo(0) - } - - @Test - fun `M remove global attributes W removeAttribute() {multithreaded}`( - @StringForgery key: String - ) { - for (i in 0..128) { - GlobalRum.addAttribute("$key$i", "value-$i") - } - var errors = 0 - val countDownLatch = CountDownLatch(2) - val threadAdd = Thread { - try { - for (i in 0..128) { - GlobalRum.removeAttribute("$key$i") - } - } catch (e: Exception) { - errors++ - } finally { - countDownLatch.countDown() - } - } - - val threadRead = Thread { - try { - for (i in 0..128) { - val iterator = GlobalRum.globalAttributes.iterator() - while (iterator.hasNext()) { - val entry = iterator.next() - println("${entry.key} = ${entry.value}") - } - } - } catch (e: Exception) { - errors++ - } finally { - countDownLatch.countDown() - } - } - - threadRead.start() - threadAdd.start() - - countDownLatch.await() - - assertThat(errors).isEqualTo(0) - } - - @Test - fun `M update plugins W updateRumContext()`(forge: Forge) { - - // Given - val applicationId = forge.aNumericalString() - val sessionId = forge.aNumericalString() - val viewId = forge.aNumericalString() - val crashFeaturePlugins: MutableList = - forge.aList(forge.anInt(min = 1, max = 4)) { - mock() - }.toMutableList() - val tracesFeaturePlugins: MutableList = - forge.aList(forge.anInt(min = 1, max = 4)) { - mock() - }.toMutableList() - val logsFeaturePlugins: MutableList = - forge.aList(forge.anInt(min = 1, max = 4)) { - mock() - }.toMutableList() - val rumFeaturePlugins: MutableList = - forge.aList(forge.anInt(min = 1, max = 4)) { - mock() - }.toMutableList() - - CrashReportsFeature.setFieldValue("featurePlugins", crashFeaturePlugins) - LogsFeature.setFieldValue("featurePlugins", logsFeaturePlugins) - TracesFeature.setFieldValue("featurePlugins", tracesFeaturePlugins) - RumFeature.setFieldValue("featurePlugins", rumFeaturePlugins) - - // When - GlobalRum.updateRumContext( - RumContext( - applicationId, - sessionId, - viewId - ) - ) - - // Then - val pluginsToAssert = - crashFeaturePlugins + tracesFeaturePlugins + logsFeaturePlugins + rumFeaturePlugins - pluginsToAssert.forEach { - verify(it).onContextChanged( - argThat { - this.rum?.applicationId == applicationId && - this.rum?.sessionId == sessionId && - this.rum?.viewId == viewId - } - ) - } - } - - @Test - fun `M reset monitor W resetSession()`() { - // Given - val monitor: AdvancedRumMonitor = mock() - GlobalRum.registerIfAbsent(monitor) - - // When - GlobalRum.invokeMethod("resetSession") - - // Then - verify(monitor).resetSession() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/MixedViewTrackingStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/MixedViewTrackingStrategyTest.kt deleted file mode 100644 index 6d08952447..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/MixedViewTrackingStrategyTest.kt +++ /dev/null @@ -1,144 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import android.os.Bundle -import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy -import com.datadog.android.rum.tracking.FragmentViewTrackingStrategy -import com.datadog.android.rum.tracking.MixedViewTrackingStrategy -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.inOrder -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class MixedViewTrackingStrategyTest : ActivityLifecycleTrackingStrategyTest() { - - @Mock - lateinit var mockActivityViewTrackingStrategy: ActivityViewTrackingStrategy - - @Mock - lateinit var mockFragmentViewTrackingStrategy: FragmentViewTrackingStrategy - - @Mock - lateinit var mockBundle: Bundle - - // region tests - - @BeforeEach - override fun `set up`(forge: Forge) { - super.`set up`(forge) - testedStrategy = - MixedViewTrackingStrategy( - mockActivityViewTrackingStrategy, - mockFragmentViewTrackingStrategy - ) - } - - @Test - fun `when created will delegate to the bundled strategies`( - forge: Forge - ) { - - // Whenever - testedStrategy.onActivityCreated(mockActivity, mockBundle) - - // Then - inOrder(mockActivityViewTrackingStrategy, mockFragmentViewTrackingStrategy) { - verify(mockActivityViewTrackingStrategy).onActivityCreated(mockActivity, mockBundle) - verify(mockFragmentViewTrackingStrategy).onActivityCreated(mockActivity, mockBundle) - } - } - - @Test - fun `when destroyed will delegate to the bundled strategies`( - forge: Forge - ) { - // Whenever - testedStrategy.onActivityDestroyed(mockActivity) - - // Then - inOrder(mockActivityViewTrackingStrategy, mockFragmentViewTrackingStrategy) { - verify(mockActivityViewTrackingStrategy).onActivityDestroyed(mockActivity) - verify(mockFragmentViewTrackingStrategy).onActivityDestroyed(mockActivity) - } - } - - @Test - fun `when started will delegate to the bundled strategies`( - forge: Forge - ) { - - // Whenever - testedStrategy.onActivityStarted(mockActivity) - - // Then - inOrder(mockActivityViewTrackingStrategy, mockFragmentViewTrackingStrategy) { - verify(mockActivityViewTrackingStrategy).onActivityStarted(mockActivity) - verify(mockFragmentViewTrackingStrategy).onActivityStarted(mockActivity) - } - } - - @Test - fun `when stopped will delegate to the bundled strategies`( - forge: Forge - ) { - // Whenever - testedStrategy.onActivityStopped(mockActivity) - - // Then - inOrder(mockActivityViewTrackingStrategy, mockFragmentViewTrackingStrategy) { - verify(mockActivityViewTrackingStrategy).onActivityStopped(mockActivity) - verify(mockFragmentViewTrackingStrategy).onActivityStopped(mockActivity) - } - } - - @Test - fun `when resumed will delegate to the bundled strategies`( - forge: Forge - ) { - - // Whenever - testedStrategy.onActivityResumed(mockActivity) - - // Then - inOrder(mockActivityViewTrackingStrategy, mockFragmentViewTrackingStrategy) { - verify(mockActivityViewTrackingStrategy).onActivityResumed(mockActivity) - verify(mockFragmentViewTrackingStrategy).onActivityResumed(mockActivity) - } - } - - @Test - fun `when paused will delegate to the bundled strategies`( - forge: Forge - ) { - // Whenever - testedStrategy.onActivityPaused(mockActivity) - - // Then - inOrder(mockActivityViewTrackingStrategy, mockFragmentViewTrackingStrategy) { - verify(mockActivityViewTrackingStrategy).onActivityPaused(mockActivity) - verify(mockFragmentViewTrackingStrategy).onActivityPaused(mockActivity) - } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumMonitorBuilderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumMonitorBuilderTest.kt deleted file mode 100644 index cc427e67fb..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumMonitorBuilderTest.kt +++ /dev/null @@ -1,158 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import android.content.Context -import android.os.Looper -import android.util.Log -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.scope.RumApplicationScope -import com.datadog.android.rum.internal.monitor.DatadogRumMonitor -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.android.utils.mockDevLogHandler -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.FloatForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.net.URL -import java.util.UUID -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumMonitorBuilderTest { - - lateinit var testedBuilder: RumMonitor.Builder - - @Mock - lateinit var mockWriter: Writer - - @Mock - lateinit var mockContext: Context - - lateinit var fakeConfig: DatadogConfig.RumConfig - - @Forgery - lateinit var fakeApplicationId: UUID - - @FloatForgery - var fakeSamplingRate: Float = 0f - - lateinit var mockDevLogHandler: LogHandler - - @BeforeEach - fun `set up`(forge: Forge) { - mockDevLogHandler = mockDevLogHandler() - fakeConfig = DatadogConfig.RumConfig( - clientToken = forge.anHexadecimalString(), - applicationId = fakeApplicationId, - endpointUrl = forge.getForgery().toString(), - envName = forge.anAlphabeticalString(), - samplingRate = fakeSamplingRate - ) - mockContext = mockContext() - - RumFeature.initialize( - mockContext, - fakeConfig, - mock(), - mock(), - mock(), - mock(), - mock(), - mock(), - TrackingConsentProvider() - ) - - testedBuilder = RumMonitor.Builder() - } - - @AfterEach - fun `tear down`() { - RumFeature.stop() - } - - @Test - fun `𝕄 builds a default RumMonitor 𝕎 build()`() { - // When - val monitor = testedBuilder.build() - - // Then - check(monitor is DatadogRumMonitor) - assertThat(monitor.rootScope).isInstanceOf(RumApplicationScope::class.java) - assertThat(monitor.rootScope) - .overridingErrorMessage("Expecting rootscope to have applicationId $fakeApplicationId") - .matches { - (it as RumApplicationScope) - .getRumContext() - .applicationId == fakeApplicationId.toString() - } - assertThat(monitor.handler.looper).isSameAs(Looper.getMainLooper()) - assertThat(monitor.samplingRate).isEqualTo(fakeSamplingRate) - } - - @Test - fun `𝕄 builds a RumMonitor with custom sampling 𝕎 build()`( - @FloatForgery(0f, 100f) samplingRate: Float - ) { - // When - val monitor = testedBuilder - .sampleRumSessions(samplingRate) - .build() - - // Then - check(monitor is DatadogRumMonitor) - assertThat(monitor.rootScope).isInstanceOf(RumApplicationScope::class.java) - assertThat(monitor.rootScope) - .overridingErrorMessage("Expecting rootscope to have applicationId $fakeApplicationId") - .matches { - (it as RumApplicationScope) - .getRumContext() - .applicationId == fakeApplicationId.toString() - } - assertThat(monitor.handler.looper).isSameAs(Looper.getMainLooper()) - assertThat(monitor.samplingRate).isEqualTo(samplingRate) - } - - @Test - fun `𝕄 builds nothing 𝕎 build() and RumFeature is not initialized`() { - // Given - RumFeature.stop() - - // When - val monitor = testedBuilder.build() - - // Then - verify(mockDevLogHandler).handleLog( - Log.ERROR, - RumMonitor.Builder.RUM_NOT_ENABLED_ERROR_MESSAGE - ) - check(monitor is NoOpRumMonitor) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumWebChromeClientTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumWebChromeClientTest.kt deleted file mode 100644 index 9983735b6a..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumWebChromeClientTest.kt +++ /dev/null @@ -1,195 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import android.util.Log -import android.webkit.ConsoleMessage -import android.webkit.WebChromeClient -import com.datadog.android.log.Logger -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.rum.webview.RumWebChromeClient -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumWebChromeClientTest { - - private lateinit var testedClient: WebChromeClient - - @Mock - private lateinit var mockLogHandler: LogHandler - - @Mock - private lateinit var mockRumMonitor: RumMonitor - - @Mock - private lateinit var mockConsoleMessage: ConsoleMessage - - @StringForgery - private lateinit var fakeMessage: String - - @StringForgery - private lateinit var fakeSource: String - - @IntForgery(min = 0) - private var fakeLine: Int = 0 - - @BeforeEach - fun `set up`() { - whenever(mockConsoleMessage.message()) doReturn fakeMessage - whenever(mockConsoleMessage.sourceId()) doReturn fakeSource - whenever(mockConsoleMessage.lineNumber()) doReturn fakeLine - - GlobalRum.registerIfAbsent(mockRumMonitor) - testedClient = RumWebChromeClient( - Logger(mockLogHandler) - ) - } - - @AfterEach - fun `tear down`() { - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.isRegistered.set(false) - } - - @Test - fun `onConsoleMessage forwards verbose log`() { - whenever(mockConsoleMessage.messageLevel()) doReturn ConsoleMessage.MessageLevel.LOG - - val result = testedClient.onConsoleMessage(mockConsoleMessage) - - verify(mockLogHandler).handleLog( - Log.VERBOSE, - fakeMessage, - null, - mapOf( - RumWebChromeClient.SOURCE_ID to fakeSource, - RumWebChromeClient.SOURCE_LINE to fakeLine - ), - emptySet(), - null - ) - verifyZeroInteractions(mockRumMonitor) - assertThat(result).isFalse() - } - - @Test - fun `onConsoleMessage forwards debug log`() { - whenever(mockConsoleMessage.messageLevel()) doReturn ConsoleMessage.MessageLevel.DEBUG - - val result = testedClient.onConsoleMessage(mockConsoleMessage) - - verify(mockLogHandler).handleLog( - Log.DEBUG, - fakeMessage, - null, - mapOf( - RumWebChromeClient.SOURCE_ID to fakeSource, - RumWebChromeClient.SOURCE_LINE to fakeLine - ), - emptySet(), - null - ) - verifyZeroInteractions(mockRumMonitor) - assertThat(result).isFalse() - } - - @Test - fun `onConsoleMessage forwards info log`() { - whenever(mockConsoleMessage.messageLevel()) doReturn ConsoleMessage.MessageLevel.TIP - - val result = testedClient.onConsoleMessage(mockConsoleMessage) - - verify(mockLogHandler).handleLog( - Log.INFO, - fakeMessage, - null, - mapOf( - RumWebChromeClient.SOURCE_ID to fakeSource, - RumWebChromeClient.SOURCE_LINE to fakeLine - ), - emptySet(), - null - ) - verifyZeroInteractions(mockRumMonitor) - assertThat(result).isFalse() - } - - @Test - fun `onConsoleMessage forwards warning log`() { - whenever(mockConsoleMessage.messageLevel()) doReturn ConsoleMessage.MessageLevel.WARNING - - val result = testedClient.onConsoleMessage(mockConsoleMessage) - - verify(mockLogHandler).handleLog( - Log.WARN, - fakeMessage, - null, - mapOf( - RumWebChromeClient.SOURCE_ID to fakeSource, - RumWebChromeClient.SOURCE_LINE to fakeLine - ), - emptySet(), - null - ) - verifyZeroInteractions(mockRumMonitor) - assertThat(result).isFalse() - } - - @Test - fun `onConsoleMessage forwards error log and sends RUM Error`() { - whenever(mockConsoleMessage.messageLevel()) doReturn ConsoleMessage.MessageLevel.ERROR - - val result = testedClient.onConsoleMessage(mockConsoleMessage) - - verifyZeroInteractions(mockLogHandler) - verify(mockRumMonitor).addError( - fakeMessage, - RumErrorSource.WEBVIEW, - null, - mapOf( - RumWebChromeClient.SOURCE_ID to fakeSource, - RumWebChromeClient.SOURCE_LINE to fakeLine - ) - ) - assertThat(result).isFalse() - } - - @Test - fun `onConsoleMessage with null message doesn't do anything`() { - val result = testedClient.onConsoleMessage(null) - - verifyZeroInteractions(mockLogHandler, mockRumMonitor) - assertThat(result).isFalse() - } - - @Test - fun testOnConsoleMessage() { - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumWebViewClientTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumWebViewClientTest.kt deleted file mode 100644 index 9663ff471b..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/RumWebViewClientTest.kt +++ /dev/null @@ -1,336 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum - -import android.graphics.Bitmap -import android.net.Uri -import android.net.http.SslError -import android.os.Build -import android.webkit.WebResourceError -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import android.webkit.WebView -import android.webkit.WebViewClient -import com.datadog.android.rum.webview.RumWebViewClient -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumWebViewClientTest { - - private lateinit var testedClient: WebViewClient - - @Mock - private lateinit var mockRumMonitor: RumMonitor - - @Mock - private lateinit var mockWebView: WebView - - @Mock - private lateinit var mockBitmap: Bitmap - - @RegexForgery("http(s?)://[a-z]+\\.com/\\w+") - private lateinit var fakeUrl: String - - @BeforeEach - fun `set up`() { - GlobalRum.registerIfAbsent(mockRumMonitor) - testedClient = RumWebViewClient() - } - - @AfterEach - fun `tear down`() { - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.isRegistered.set(false) - } - - @Test - fun `onPageStarted starts a RUM Resource`() { - testedClient.onPageStarted(mockWebView, fakeUrl, mockBitmap) - - verify(mockRumMonitor).startResource( - fakeUrl, - "GET", - fakeUrl, - emptyMap() - ) - } - - @Test - fun `onPageStarted with null URL does nothing`() { - testedClient.onPageStarted(mockWebView, null, mockBitmap) - - verifyZeroInteractions(mockRumMonitor) - } - - @Test - fun `onPageFinished stops a RUM Resource`() { - testedClient.onPageFinished(mockWebView, fakeUrl) - - verify(mockRumMonitor).stopResource( - fakeUrl, - 200, - null, - RumResourceKind.DOCUMENT, - emptyMap() - ) - } - - @Test - fun `onPageFinished with null URL does nothing`() { - testedClient.onPageFinished(mockWebView, null) - - verifyZeroInteractions(mockRumMonitor) - } - - @Test - fun `onReceivedError sends a RUM Error`( - @IntForgery errorCode: Int, - @StringForgery description: String - ) { - testedClient.onReceivedError(mockWebView, errorCode, description, fakeUrl) - - verify(mockRumMonitor).addError( - "Error $errorCode: $description", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to fakeUrl) - ) - } - - @Test - fun `onReceivedError with null description sends a RUM Error`( - @IntForgery errorCode: Int, - @StringForgery description: String - ) { - testedClient.onReceivedError(mockWebView, errorCode, null, fakeUrl) - - verify(mockRumMonitor).addError( - "Error $errorCode: null", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to fakeUrl) - ) - } - - @Test - fun `onReceivedError with null url sends a RUM Error`( - @IntForgery errorCode: Int, - @StringForgery description: String - ) { - testedClient.onReceivedError(mockWebView, errorCode, description, null) - - verify(mockRumMonitor).addError( - "Error $errorCode: $description", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to null) - ) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.M) - fun `onReceivedError (request) sends a RUM Error`( - @IntForgery errorCode: Int, - @StringForgery description: String - ) { - val mockRequest: WebResourceRequest = mock() - val mockError: WebResourceError = mock() - val mockUri: Uri = mock() - whenever(mockRequest.url) doReturn mockUri - whenever(mockError.description) doReturn description - whenever(mockError.errorCode) doReturn errorCode - - testedClient.onReceivedError(mockWebView, mockRequest, mockError) - - verify(mockRumMonitor).addError( - "Error $errorCode: $description", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to mockUri) - ) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.M) - fun `onReceivedError (request) with null request sends a RUM Error`( - @IntForgery errorCode: Int, - @StringForgery description: String - ) { - val mockError: WebResourceError = mock() - whenever(mockError.description) doReturn description - whenever(mockError.errorCode) doReturn errorCode - - testedClient.onReceivedError(mockWebView, null, mockError) - - verify(mockRumMonitor).addError( - "Error $errorCode: $description", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to null) - ) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.M) - fun `onReceivedError (request) with null error sends a RUM Error`( - @IntForgery errorCode: Int, - @StringForgery description: String - ) { - val mockRequest: WebResourceRequest = mock() - val mockUri: Uri = mock() - whenever(mockRequest.url) doReturn mockUri - - testedClient.onReceivedError(mockWebView, mockRequest, null) - - verify(mockRumMonitor).addError( - "Error null: null", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to mockUri) - ) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `onReceivedHttpError sends a RUM Error`( - @IntForgery statusCode: Int, - @StringForgery reasonPhrase: String - ) { - val mockRequest: WebResourceRequest = mock() - val mockResponse: WebResourceResponse = mock() - val mockUri: Uri = mock() - whenever(mockRequest.url) doReturn mockUri - whenever(mockResponse.reasonPhrase) doReturn reasonPhrase - whenever(mockResponse.statusCode) doReturn statusCode - - testedClient.onReceivedHttpError(mockWebView, mockRequest, mockResponse) - - verify(mockRumMonitor).addError( - "Error $statusCode: $reasonPhrase", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to mockUri) - ) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `onReceivedHttpError with null response sends a RUM Error`( - @IntForgery statusCode: Int, - @StringForgery reasonPhrase: String - ) { - val mockRequest: WebResourceRequest = mock() - val mockUri: Uri = mock() - whenever(mockRequest.url) doReturn mockUri - - testedClient.onReceivedHttpError(mockWebView, mockRequest, null) - - verify(mockRumMonitor).addError( - "Error null: null", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to mockUri) - ) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) - fun `onReceivedHttpError with null request sends a RUM Error`( - @IntForgery statusCode: Int, - @StringForgery reasonPhrase: String - ) { - val mockResponse: WebResourceResponse = mock() - whenever(mockResponse.reasonPhrase) doReturn reasonPhrase - whenever(mockResponse.statusCode) doReturn statusCode - - testedClient.onReceivedHttpError(mockWebView, null, mockResponse) - - verify(mockRumMonitor).addError( - "Error $statusCode: $reasonPhrase", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to null) - ) - } - - @Test - fun `onReceivedSslError sends a RUM Error`( - @IntForgery primaryError: Int - ) { - val mockError: SslError = mock() - whenever(mockError.primaryError) doReturn primaryError - whenever(mockError.url) doReturn fakeUrl - - testedClient.onReceivedSslError(mockWebView, mock(), mockError) - - verify(mockRumMonitor).addError( - "SSL Error $primaryError", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to fakeUrl) - ) - } - - @Test - fun `onReceivedSslError with null handler sends a RUM Error`( - @IntForgery primaryError: Int - ) { - val mockError: SslError = mock() - whenever(mockError.primaryError) doReturn primaryError - whenever(mockError.url) doReturn fakeUrl - - testedClient.onReceivedSslError(mockWebView, null, mockError) - - verify(mockRumMonitor).addError( - "SSL Error $primaryError", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to fakeUrl) - ) - } - - @Test - fun `onReceivedSslError with null error sends a RUM Error`( - @IntForgery primaryError: Int - ) { - testedClient.onReceivedSslError(mockWebView, mock(), null) - - verify(mockRumMonitor).addError( - "SSL Error null", - RumErrorSource.WEBVIEW, - null, - mapOf(RumAttributes.ERROR_RESOURCE_URL to null) - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ActionEventAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ActionEventAssert.kt deleted file mode 100644 index 7a35cca4ab..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ActionEventAssert.kt +++ /dev/null @@ -1,215 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.assertj - -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.model.ActionEvent -import com.datadog.android.rum.internal.domain.scope.toSchemaType -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset - -internal class ActionEventAssert(actual: ActionEvent) : - AbstractObjectAssert( - actual, - ActionEventAssert::class.java - ) { - - fun hasId(expected: String): ActionEventAssert { - assertThat(actual.action.id) - .overridingErrorMessage( - "Expected event data to have action.id $expected " + - "but was ${actual.action.id}" - ) - .isNotEqualTo(RumContext.NULL_UUID) - .isEqualTo(expected) - return this - } - - fun hasNonNullId(): ActionEventAssert { - assertThat(actual.action.id) - .overridingErrorMessage( - "Expected event data to have non null action.id " + - "but was ${actual.action.id}" - ) - .isNotNull() - .isNotEqualTo(RumContext.NULL_UUID) - return this - } - - fun hasTimestamp( - expected: Long, - offset: Long = RumEventAssert.TIMESTAMP_THRESHOLD_MS - ): ActionEventAssert { - assertThat(actual.date) - .overridingErrorMessage( - "Expected event to have timestamp $expected but was ${actual.date}" - ) - .isCloseTo(expected, Offset.offset(offset)) - return this - } - - fun hasType(expected: RumActionType): ActionEventAssert { - assertThat(actual.action.type) - .overridingErrorMessage( - "Expected event data to have action.type $expected but was ${actual.action.type}" - ) - .isEqualTo(expected.toSchemaType()) - return this - } - - fun hasType(expected: ActionEvent.Type1): ActionEventAssert { - assertThat(actual.action.type) - .overridingErrorMessage( - "Expected event data to have action.type $expected but was ${actual.action.type}" - ) - .isEqualTo(expected) - return this - } - - fun hasNoTarget(): ActionEventAssert { - assertThat(actual.action.target) - .overridingErrorMessage( - "Expected event data to have no action.target " + - "but was ${actual.action.target}" - ) - .isNull() - return this - } - - fun hasTargetName(expected: String): ActionEventAssert { - assertThat(actual.action.target?.name) - .overridingErrorMessage( - "Expected event data to have action.target.name $expected " + - "but was ${actual.action.target?.name}" - ) - .isEqualTo(expected) - return this - } - - fun hasResourceCount(expected: Long): ActionEventAssert { - assertThat(actual.action.resource?.count ?: 0) - .overridingErrorMessage( - "Expected event data to have action.resource.count $expected " + - "but was ${actual.action.resource?.count}" - ) - .isEqualTo(expected) - return this - } - - fun hasErrorCount(expected: Long): ActionEventAssert { - assertThat(actual.action.error?.count ?: 0) - .overridingErrorMessage( - "Expected event data to have action.error.count $expected " + - "but was ${actual.action.error?.count}" - ) - .isEqualTo(expected) - return this - } - - fun hasCrashCount(expected: Long): ActionEventAssert { - assertThat(actual.action.crash?.count ?: 0) - .overridingErrorMessage( - "Expected event data to have action.crash.count $expected " + - "but was ${actual.action.crash?.count}" - ) - .isEqualTo(expected) - return this - } - - fun hasView(expectedId: String?, expectedUrl: String?): ActionEventAssert { - assertThat(actual.view.id) - .overridingErrorMessage( - "Expected event data to have view.id $expectedId but was ${actual.view.id}" - ) - .isEqualTo(expectedId.orEmpty()) - assertThat(actual.view.url) - .overridingErrorMessage( - "Expected event data to have view.url $expectedUrl but was ${actual.view.url}" - ) - .isEqualTo(expectedUrl.orEmpty()) - return this - } - - fun hasUserInfo(expected: UserInfo?): ActionEventAssert { - assertThat(actual.usr?.id) - .overridingErrorMessage( - "Expected RUM event to have usr.id ${expected?.id} " + - "but was ${actual.usr?.id}" - ) - .isEqualTo(expected?.id) - assertThat(actual.usr?.name) - .overridingErrorMessage( - "Expected RUM event to have usr.name ${expected?.name} " + - "but was ${actual.usr?.name}" - ) - .isEqualTo(expected?.name) - assertThat(actual.usr?.email) - .overridingErrorMessage( - "Expected RUM event to have usr.email ${expected?.email} " + - "but was ${actual.usr?.email}" - ) - .isEqualTo(expected?.email) - return this - } - - fun hasApplicationId(expected: String): ActionEventAssert { - assertThat(actual.application.id) - .overridingErrorMessage( - "Expected context to have application.id $expected but was ${actual.application.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasSessionId(expected: String): ActionEventAssert { - assertThat(actual.session.id) - .overridingErrorMessage( - "Expected context to have session.id $expected but was ${actual.session.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasDuration(expected: Long): ActionEventAssert { - assertThat(actual.action.loadingTime) - .overridingErrorMessage( - "Expected event data to have duration $expected " + - "but was ${actual.action.loadingTime}" - ) - .isEqualTo(expected) - return this - } - - fun hasDurationLowerThan(upperBound: Long): ActionEventAssert { - assertThat(actual.action.loadingTime) - .overridingErrorMessage( - "Expected event data to have duration lower than $upperBound " + - "but was ${actual.action.loadingTime}" - ) - .isLessThanOrEqualTo(upperBound) - return this - } - - fun hasDurationGreaterThan(lowerBound: Long): ActionEventAssert { - assertThat(actual.action.loadingTime) - .overridingErrorMessage( - "Expected event data to have duration greater than $lowerBound " + - "but was ${actual.action.loadingTime}" - ) - .isGreaterThanOrEqualTo(lowerBound) - return this - } - - companion object { - - internal fun assertThat(actual: ActionEvent): ActionEventAssert = - ActionEventAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt deleted file mode 100644 index b41796b574..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt +++ /dev/null @@ -1,222 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.assertj - -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.scope.toSchemaSource -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset - -internal class ErrorEventAssert(actual: ErrorEvent) : - AbstractObjectAssert( - actual, - ErrorEventAssert::class.java - ) { - - fun hasTimestamp( - expected: Long, - offset: Long = RumEventAssert.TIMESTAMP_THRESHOLD_MS - ): ErrorEventAssert { - assertThat(actual.date) - .overridingErrorMessage( - "Expected event to have timestamp $expected but was ${actual.date}" - ) - .isCloseTo(expected, Offset.offset(offset)) - return this - } - - fun hasSource(expected: RumErrorSource): ErrorEventAssert { - assertThat(actual.error.source) - .overridingErrorMessage( - "Expected event data to have error.source $expected but was ${actual.error.source}" - ) - .isEqualTo(expected.toSchemaSource()) - return this - } - - fun hasMessage(expected: String): ErrorEventAssert { - assertThat(actual.error.message) - .overridingErrorMessage( - "Expected event data to have error.message $expected " + - "but was ${actual.error.message}" - ) - .isEqualTo(expected) - return this - } - - fun hasStackTrace(expected: String?): ErrorEventAssert { - assertThat(actual.error.stack) - .overridingErrorMessage( - "Expected event data to have error.stack $expected but was ${actual.error.stack}" - ) - .isEqualTo(expected) - return this - } - - fun isCrash(expected: Boolean): ErrorEventAssert { - assertThat(actual.error.isCrash) - .overridingErrorMessage( - "Expected event data to have error.isCrash $expected " + - "but was ${actual.error.isCrash}" - ) - .isEqualTo(expected) - return this - } - - fun hasResource( - expectedUrl: String, - expectedMethod: String, - expectedStatusCode: Long - ): ErrorEventAssert { - assertThat(actual.error.resource?.url) - .overridingErrorMessage( - "Expected event data to have error.resource.url $expectedUrl " + - "but was ${actual.error.resource?.url}" - ) - .isEqualTo(expectedUrl) - assertThat(actual.error.resource?.method) - .overridingErrorMessage( - "Expected event data to have error.resource.method $expectedMethod " + - "but was ${actual.error.resource?.method}" - ) - .isEqualTo(ErrorEvent.Method.valueOf(expectedMethod)) - assertThat(actual.error.resource?.statusCode) - .overridingErrorMessage( - "Expected event data to have error.resource.statusCode $expectedStatusCode " + - "but was ${actual.error.resource?.statusCode}" - ) - .isEqualTo(expectedStatusCode) - return this - } - - fun hasUserInfo(expected: UserInfo?): ErrorEventAssert { - assertThat(actual.usr?.id) - .overridingErrorMessage( - "Expected RUM event to have usr.id ${expected?.id} " + - "but was ${actual.usr?.id}" - ) - .isEqualTo(expected?.id) - assertThat(actual.usr?.name) - .overridingErrorMessage( - "Expected RUM event to have usr.name ${expected?.name} " + - "but was ${actual.usr?.name}" - ) - .isEqualTo(expected?.name) - assertThat(actual.usr?.email) - .overridingErrorMessage( - "Expected RUM event to have usr.email ${expected?.email} " + - "but was ${actual.usr?.email}" - ) - .isEqualTo(expected?.email) - return this - } - - fun hasConnectivityInfo(expected: NetworkInfo?): ErrorEventAssert { - val expectedStatus = if (expected?.isConnected() == true) { - ErrorEvent.Status.CONNECTED - } else { - ErrorEvent.Status.NOT_CONNECTED - } - val expectedInterfaces = when (expected?.connectivity) { - NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ErrorEvent.Interface.ETHERNET) - NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ErrorEvent.Interface.WIFI) - NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ErrorEvent.Interface.WIMAX) - NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ErrorEvent.Interface.BLUETOOTH) - NetworkInfo.Connectivity.NETWORK_2G, - NetworkInfo.Connectivity.NETWORK_3G, - NetworkInfo.Connectivity.NETWORK_4G, - NetworkInfo.Connectivity.NETWORK_5G, - NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, - NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ErrorEvent.Interface.CELLULAR) - NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ErrorEvent.Interface.OTHER) - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() - null -> null - } - - assertThat(actual.connectivity?.status) - .overridingErrorMessage( - "Expected RUM event to have connectivity.status $expectedStatus " + - "but was ${actual.connectivity?.status}" - ) - .isEqualTo(expectedStatus) - - assertThat(actual.connectivity?.cellular?.technology) - .overridingErrorMessage( - "Expected RUM event to connectivity usr.cellular.technology " + - "${expected?.cellularTechnology} " + - "but was ${actual.connectivity?.cellular?.technology}" - ) - .isEqualTo(expected?.cellularTechnology) - - assertThat(actual.connectivity?.cellular?.carrierName) - .overridingErrorMessage( - "Expected RUM event to connectivity usr.cellular.carrierName " + - "${expected?.carrierName} " + - "but was ${actual.connectivity?.cellular?.carrierName}" - ) - .isEqualTo(expected?.carrierName) - - assertThat(actual.connectivity?.interfaces) - .overridingErrorMessage( - "Expected RUM event to have connectivity.interfaces $expectedInterfaces " + - "but was ${actual.connectivity?.interfaces}" - ) - .isEqualTo(expectedInterfaces) - return this - } - - fun hasView(expectedId: String?, expectedUrl: String?): ErrorEventAssert { - assertThat(actual.view.id) - .overridingErrorMessage( - "Expected event data to have view.id $expectedId but was ${actual.view.id}" - ) - .isEqualTo(expectedId.orEmpty()) - assertThat(actual.view.url) - .overridingErrorMessage( - "Expected event data to have view.id $expectedUrl but was ${actual.view.url}" - ) - .isEqualTo(expectedUrl.orEmpty()) - return this - } - - fun hasApplicationId(expected: String): ErrorEventAssert { - assertThat(actual.application.id) - .overridingErrorMessage( - "Expected context to have application.id $expected but was ${actual.application.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasSessionId(expected: String): ErrorEventAssert { - assertThat(actual.session.id) - .overridingErrorMessage( - "Expected context to have session.id $expected but was ${actual.session.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasActionId(expected: String?): ErrorEventAssert { - assertThat(actual.action?.id) - .overridingErrorMessage( - "Expected event data to have action.id $expected but was ${actual.action?.id}" - ) - .isEqualTo(expected) - return this - } - - companion object { - - internal fun assertThat(actual: ErrorEvent): ErrorEventAssert = - ErrorEventAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ResourceEventAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ResourceEventAssert.kt deleted file mode 100644 index d5fa9b76bc..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ResourceEventAssert.kt +++ /dev/null @@ -1,386 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.assertj - -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.model.ResourceEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.domain.scope.toMethod -import com.datadog.android.rum.internal.domain.scope.toSchemaType -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset - -internal class ResourceEventAssert(actual: ResourceEvent) : - AbstractObjectAssert( - actual, - ResourceEventAssert::class.java - ) { - - fun hasId(expected: String): ResourceEventAssert { - assertThat(actual.resource.id) - .overridingErrorMessage( - "Expected event data to have resource.id $expected " + - "but was ${actual.resource.id}" - ) - .isNotEqualTo(RumContext.NULL_UUID) - .isEqualTo(expected) - return this - } - - fun hasTimestamp( - expected: Long, - offset: Long = RumEventAssert.TIMESTAMP_THRESHOLD_MS - ): ResourceEventAssert { - assertThat(actual.date) - .overridingErrorMessage( - "Expected event to have timestamp $expected but was ${actual.date}" - ) - .isCloseTo(expected, Offset.offset(offset)) - return this - } - - fun hasKind(expected: RumResourceKind): ResourceEventAssert { - assertThat(actual.resource.type) - .overridingErrorMessage( - "Expected event data to have resource.type $expected " + - "but was ${actual.resource.type}" - ) - .isEqualTo(expected.toSchemaType()) - return this - } - - fun hasUrl(expected: String): ResourceEventAssert { - assertThat(actual.resource.url) - .overridingErrorMessage( - "Expected event data to have resource.url $expected but was ${actual.resource.url}" - ) - .isEqualTo(expected) - return this - } - - fun hasMethod(expected: String): ResourceEventAssert { - assertThat(actual.resource.method) - .overridingErrorMessage( - "Expected event data to have resource.method $expected " + - "but was ${actual.resource.method}" - ) - .isEqualTo(expected.toMethod()) - return this - } - - fun hasDurationGreaterThan(upperBound: Long): ResourceEventAssert { - assertThat(actual.resource.duration) - .overridingErrorMessage( - "Expected event data to have resource.duration greater than $upperBound " + - "but was ${actual.resource.duration}" - ) - .isGreaterThanOrEqualTo(upperBound) - return this - } - - fun hasTiming(expected: ResourceTiming): ResourceEventAssert { - if (expected.dnsDuration > 0) { - assertThat(actual.resource.dns?.start) - .overridingErrorMessage( - "Expected event data to have resource.dns.start ${expected.dnsStart} " + - "but was ${actual.resource.dns?.start}" - ) - .isEqualTo(expected.dnsStart) - assertThat(actual.resource.dns?.duration) - .overridingErrorMessage( - "Expected event data to have resource.dns.duration ${expected.dnsDuration} " + - "but was ${actual.resource.dns?.duration}" - ) - .isEqualTo(expected.dnsDuration) - } else { - assertThat(actual.resource.dns) - .overridingErrorMessage( - "Expected event data to have no resource.dns but was ${actual.resource.dns}" - ) - .isNull() - } - - if (expected.connectDuration > 0) { - assertThat(actual.resource.connect?.start) - .overridingErrorMessage( - "Expected event data to have resource.connect.start ${expected.connectStart} " + - "but was ${actual.resource.connect?.start}" - ) - .isEqualTo(expected.connectStart) - assertThat(actual.resource.connect?.duration) - .overridingErrorMessage( - "Expected event data to have resource.connect.duration " + - "${expected.connectDuration} but was ${actual.resource.connect?.duration}" - ) - .isEqualTo(expected.connectDuration) - } else { - assertThat(actual.resource.connect) - .overridingErrorMessage( - "Expected event data to have no resource.connect " + - "but was ${actual.resource.connect}" - ) - .isNull() - } - - if (expected.sslDuration > 0) { - assertThat(actual.resource.ssl?.start) - .overridingErrorMessage( - "Expected event data to have resource.ssl.start ${expected.sslStart} " + - "but was ${actual.resource.ssl?.start}" - ) - .isEqualTo(expected.sslStart) - assertThat(actual.resource.ssl?.duration) - .overridingErrorMessage( - "Expected event data to have resource.ssl.duration ${expected.sslDuration} " + - "but was ${actual.resource.ssl?.duration}" - ) - .isEqualTo(expected.sslDuration) - } else { - assertThat(actual.resource.ssl) - .overridingErrorMessage( - "Expected event data to have no resource.ssl but was ${actual.resource.ssl}" - ) - .isNull() - } - - if (expected.firstByteDuration > 0) { - assertThat(actual.resource.firstByte?.start) - .overridingErrorMessage( - "Expected event data to have resource.firstByte.start " + - "${expected.firstByteStart} but was ${actual.resource.firstByte?.start}" - ) - .isEqualTo(expected.firstByteStart) - assertThat(actual.resource.firstByte?.duration) - .overridingErrorMessage( - "Expected event data to have resource.firstByte.duration " + - "${expected.firstByteDuration} but was " + - "${actual.resource.firstByte?.duration}" - ) - .isEqualTo(expected.firstByteDuration) - } else { - assertThat(actual.resource.firstByte) - .overridingErrorMessage( - "Expected event data to have no resource.firstByte " + - "but was ${actual.resource.firstByte}" - ) - .isNull() - } - - if (expected.downloadDuration > 0) { - assertThat(actual.resource.download?.start) - .overridingErrorMessage( - "Expected event data to have resource.download.start " + - "${expected.downloadStart} but was ${actual.resource.download?.start}" - ) - .isEqualTo(expected.downloadStart) - assertThat(actual.resource.download?.duration) - .overridingErrorMessage( - "Expected event data to have resource.download.duration " + - "${expected.downloadDuration} but was ${actual.resource.download?.duration}" - ) - .isEqualTo(expected.downloadDuration) - } else { - assertThat(actual.resource.download) - .overridingErrorMessage( - "Expected event data to have no resource.download " + - "but was ${actual.resource.download}" - ) - .isNull() - } - return this - } - - fun hasNoTiming(): ResourceEventAssert { - assertThat(actual.resource.dns) - .overridingErrorMessage( - "Expected event data to have no resource.dns but was ${actual.resource.dns}" - ) - .isNull() - assertThat(actual.resource.connect) - .overridingErrorMessage( - "Expected event data to have no resource.connect but was ${actual.resource.connect}" - ) - .isNull() - assertThat(actual.resource.ssl) - .overridingErrorMessage( - "Expected event data to have no resource.ssl but was ${actual.resource.ssl}" - ) - .isNull() - assertThat(actual.resource.firstByte) - .overridingErrorMessage( - "Expected event data to have no resource.firstByte " + - "but was ${actual.resource.firstByte}" - ) - .isNull() - assertThat(actual.resource.download) - .overridingErrorMessage( - "Expected event data to have no resource.download " + - "but was ${actual.resource.download}" - ) - .isNull() - return this - } - - fun hasUserInfo(expected: UserInfo?): ResourceEventAssert { - assertThat(actual.usr?.id) - .overridingErrorMessage( - "Expected RUM event to have usr.id ${expected?.id} " + - "but was ${actual.usr?.id}" - ) - .isEqualTo(expected?.id) - assertThat(actual.usr?.name) - .overridingErrorMessage( - "Expected RUM event to have usr.name ${expected?.name} " + - "but was ${actual.usr?.name}" - ) - .isEqualTo(expected?.name) - assertThat(actual.usr?.email) - .overridingErrorMessage( - "Expected RUM event to have usr.email ${expected?.email} " + - "but was ${actual.usr?.email}" - ) - .isEqualTo(expected?.email) - return this - } - - fun hasConnectivityInfo(expected: NetworkInfo?): ResourceEventAssert { - val expectedStatus = if (expected?.isConnected() == true) { - ResourceEvent.Status.CONNECTED - } else { - ResourceEvent.Status.NOT_CONNECTED - } - val expectedInterfaces = when (expected?.connectivity) { - NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ResourceEvent.Interface.ETHERNET) - NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ResourceEvent.Interface.WIFI) - NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ResourceEvent.Interface.WIMAX) - NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ResourceEvent.Interface.BLUETOOTH) - NetworkInfo.Connectivity.NETWORK_2G, - NetworkInfo.Connectivity.NETWORK_3G, - NetworkInfo.Connectivity.NETWORK_4G, - NetworkInfo.Connectivity.NETWORK_5G, - NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, - NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ResourceEvent.Interface.CELLULAR) - NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ResourceEvent.Interface.OTHER) - NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() - null -> null - } - - assertThat(actual.connectivity?.status) - .overridingErrorMessage( - "Expected RUM event to have connectivity.status $expectedStatus " + - "but was ${actual.connectivity?.status}" - ) - .isEqualTo(expectedStatus) - - assertThat(actual.connectivity?.cellular?.technology) - .overridingErrorMessage( - "Expected RUM event to connectivity usr.cellular.technology " + - "${expected?.cellularTechnology} " + - "but was ${actual.connectivity?.cellular?.technology}" - ) - .isEqualTo(expected?.cellularTechnology) - - assertThat(actual.connectivity?.cellular?.carrierName) - .overridingErrorMessage( - "Expected RUM event to connectivity usr.cellular.carrierName " + - "${expected?.carrierName} " + - "but was ${actual.connectivity?.cellular?.carrierName}" - ) - .isEqualTo(expected?.carrierName) - - assertThat(actual.connectivity?.interfaces) - .overridingErrorMessage( - "Expected RUM event to have connectivity.interfaces $expectedInterfaces " + - "but was ${actual.connectivity?.interfaces}" - ) - .isEqualTo(expectedInterfaces) - return this - } - - fun hasView(expectedId: String?, expectedUrl: String?): ResourceEventAssert { - assertThat(actual.view.id) - .overridingErrorMessage( - "Expected event data to have view.id $expectedId but was ${actual.view.id}" - ) - .isEqualTo(expectedId.orEmpty()) - assertThat(actual.view.url) - .overridingErrorMessage( - "Expected event data to have view.id $expectedUrl but was ${actual.view.url}" - ) - .isEqualTo(expectedUrl.orEmpty()) - return this - } - - fun hasApplicationId(expected: String): ResourceEventAssert { - assertThat(actual.application.id) - .overridingErrorMessage( - "Expected event data to have application.id $expected " + - "but was ${actual.application.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasSessionId(expected: String): ResourceEventAssert { - assertThat(actual.session.id) - .overridingErrorMessage( - "Expected event data to have session.id $expected but was ${actual.session.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasActionId(expected: String?): ResourceEventAssert { - assertThat(actual.action?.id) - .overridingErrorMessage( - "Expected event data to have action.id $expected but was ${actual.action?.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasTraceId(expected: String?): ResourceEventAssert { - assertThat(actual.dd?.traceId) - .overridingErrorMessage( - "Expected event data to have _dd.trace_id $expected but was ${actual.dd?.traceId}" - ) - .isEqualTo(expected) - return this - } - - fun hasSpanId(expected: String?): ResourceEventAssert { - assertThat(actual.dd?.spanId) - .overridingErrorMessage( - "Expected event data to have _dd.span_id $expected but was ${actual.dd?.spanId}" - ) - .isEqualTo(expected) - return this - } - - fun hasFirstParty(expected: Boolean?): ResourceEventAssert { - assertThat(actual.resource.firstParty) - .overridingErrorMessage( - "Expected event data to have resource.first_party $expected " + - "but was ${actual.resource.firstParty}" - ) - .isEqualTo(expected) - return this - } - - companion object { - - internal const val DURATION_THRESHOLD_NANOS = 1000L - - internal fun assertThat(actual: ViewEvent): ViewEventAssert = - ViewEventAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumConfigAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumConfigAssert.kt deleted file mode 100644 index 031b84043d..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumConfigAssert.kt +++ /dev/null @@ -1,135 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.assertj - -import com.datadog.android.DatadogConfig -import com.datadog.android.rum.internal.instrumentation.GesturesTrackingStrategy -import com.datadog.android.rum.internal.instrumentation.GesturesTrackingStrategyApi29 -import com.datadog.android.rum.internal.instrumentation.gestures.DatadogGesturesTracker -import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy -import com.datadog.android.rum.tracking.TrackingStrategy -import com.datadog.android.rum.tracking.ViewAttributesProvider -import java.util.UUID -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat - -internal class RumConfigAssert(actual: DatadogConfig.RumConfig) : - AbstractObjectAssert( - actual, - RumConfigAssert::class.java - ) { - - // region Assertions - - fun hasClientToken(clientToken: String): RumConfigAssert { - assertThat(actual.clientToken) - .overridingErrorMessage( - "Expected event to have client token $clientToken" + - " but was ${actual.clientToken}" - ) - .isEqualTo(clientToken) - return this - } - - fun hasApplicationId(applicationId: UUID): RumConfigAssert { - assertThat(actual.applicationId) - .overridingErrorMessage( - "Expected event to have application id $applicationId " + - "but was ${actual.applicationId}" - ) - .isEqualTo(applicationId) - return this - } - - fun hasEndpointUrl(url: String): RumConfigAssert { - assertThat(actual.endpointUrl) - .overridingErrorMessage( - "Expected event to have endpoint url $url" + - " but was ${actual.endpointUrl}" - ) - .isEqualTo(url) - return this - } - - fun hasEnvName(envName: String): RumConfigAssert { - assertThat(actual.envName) - .overridingErrorMessage( - "Expected event to have env name $envName" + - " but was ${actual.envName}" - ) - .isEqualTo(envName) - return this - } - - fun doesNotHaveGesturesTrackingStrategy(): RumConfigAssert { - assertThat(actual.userActionTrackingStrategy).isNull() - return this - } - - fun doesNotHaveViewTrackingStrategy(): RumConfigAssert { - assertThat(actual.viewTrackingStrategy).isNull() - return this - } - - fun hasGesturesTrackingStrategyApi29( - extraAttributesProviders: Array = emptyArray() - ): RumConfigAssert { - val userActionTrackingStrategy = isInstanceOf() - val gesturesTracker = userActionTrackingStrategy.gesturesTracker as DatadogGesturesTracker - RumGestureTrackerAssert.assertThat(gesturesTracker) - .hasCustomTargetAttributesProviders(extraAttributesProviders) - .hasDefaultTargetAttributesProviders() - return this - } - - fun hasGesturesTrackingStrategy( - extraAttributesProviders: Array = emptyArray() - ): RumConfigAssert { - val userActionTrackingStrategy = isInstanceOf() - val gesturesTracker = userActionTrackingStrategy.gesturesTracker as DatadogGesturesTracker - RumGestureTrackerAssert.assertThat(gesturesTracker) - .hasCustomTargetAttributesProviders(extraAttributesProviders) - .hasDefaultTargetAttributesProviders() - return this - } - - fun hasViewTrackingStrategy(viewTrackingStrategy: TrackingStrategy): RumConfigAssert { - assertThat(actual.viewTrackingStrategy) - .overridingErrorMessage( - "Expected the viewTrackingStrategy to " + - "be $viewTrackingStrategy" + - " but was ${actual.viewTrackingStrategy}" - ) - .isEqualTo(viewTrackingStrategy) - return this - } - - // endregion - - // region Internal - - private inline fun isInstanceOf(): Strategy { - val userActionTrackingStrategy = actual.userActionTrackingStrategy as? Strategy - assertThat(userActionTrackingStrategy).isNotNull() - assertThat(userActionTrackingStrategy) - .overridingErrorMessage( - "Expected the trackGesturesStrategy " + - "to be instance of ${Strategy::class.java.canonicalName}" + - " but was ${userActionTrackingStrategy!!::class.java.canonicalName}" - ) - .isInstanceOf(Strategy::class.java) - return userActionTrackingStrategy - } - - // endregion - - companion object { - - internal fun assertThat(actual: DatadogConfig.RumConfig): RumConfigAssert = - RumConfigAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumEventAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumEventAssert.kt deleted file mode 100644 index 1226d8784a..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumEventAssert.kt +++ /dev/null @@ -1,93 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.assertj - -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.model.ActionEvent -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.model.ResourceEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import java.util.concurrent.TimeUnit -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset - -internal class RumEventAssert(actual: RumEvent) : - AbstractObjectAssert( - actual, - RumEventAssert::class.java - ) { - - fun hasAttributes(attributes: Map): RumEventAssert { - assertThat(actual.globalAttributes) - .containsAllEntriesOf(attributes) - return this - } - - fun hasUserExtraAttributes(attributes: Map): RumEventAssert { - assertThat(actual.userExtraAttributes) - .containsAllEntriesOf(attributes) - return this - } - - fun hasCustomTimings(customTimings: Map): RumEventAssert { - customTimings.entries.forEach { entry -> - assertThat(actual.customTimings).hasEntrySatisfying(entry.key) { - assertThat(it).isCloseTo( - entry.value, - Offset.offset(TimeUnit.MILLISECONDS.toNanos(10)) - ) - } - } - - return this - } - - fun hasViewData(assert: ViewEventAssert.() -> Unit): RumEventAssert { - assertThat(actual.event) - .isInstanceOf(ViewEvent::class.java) - - ViewEventAssert(actual.event as ViewEvent).assert() - - return this - } - - fun hasResourceData(assert: ResourceEventAssert.() -> Unit): RumEventAssert { - assertThat(actual.event) - .isInstanceOf(ResourceEvent::class.java) - - ResourceEventAssert(actual.event as ResourceEvent).assert() - - return this - } - - fun hasActionData(assert: ActionEventAssert.() -> Unit): RumEventAssert { - assertThat(actual.event) - .isInstanceOf(ActionEvent::class.java) - - ActionEventAssert(actual.event as ActionEvent).assert() - - return this - } - - fun hasErrorData(assert: ErrorEventAssert.() -> Unit): RumEventAssert { - assertThat(actual.event) - .isInstanceOf(ErrorEvent::class.java) - - ErrorEventAssert(actual.event as ErrorEvent).assert() - - return this - } - - companion object { - - internal const val TIMESTAMP_THRESHOLD_MS = 50L - - internal fun assertThat(actual: RumEvent): RumEventAssert = - RumEventAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumGestureTrackerAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumGestureTrackerAssert.kt deleted file mode 100644 index 1a2d8bd8e9..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/RumGestureTrackerAssert.kt +++ /dev/null @@ -1,51 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.assertj - -import com.datadog.android.rum.internal.instrumentation.gestures.DatadogGesturesTracker -import com.datadog.android.rum.internal.tracking.JetpackViewAttributesProvider -import com.datadog.android.rum.tracking.ViewAttributesProvider -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat - -internal class RumGestureTrackerAssert(actual: DatadogGesturesTracker) : - AbstractObjectAssert( - actual, - RumGestureTrackerAssert::class.java - ) { - - // region Assertions - - fun hasDefaultTargetAttributesProviders(): RumGestureTrackerAssert { - val count = actual.targetAttributesProviders - .filterIsInstance() - .count() - assertThat(count) - .overridingErrorMessage( - "We were expecting" + - " a JetpackViewAttributesProvider as a default provider" + - " but we could not find any" - ) - .isEqualTo(1) - return this - } - - fun hasCustomTargetAttributesProviders(customProviders: Array): - RumGestureTrackerAssert { - assertThat(actual.targetAttributesProviders) - .containsAll(customProviders.toMutableList()) - return this - } - - // endregion - - companion object { - - internal fun assertThat(actual: DatadogGesturesTracker): RumGestureTrackerAssert = - RumGestureTrackerAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ViewEventAssert.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ViewEventAssert.kt deleted file mode 100644 index 262ac4f68c..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/assertj/ViewEventAssert.kt +++ /dev/null @@ -1,205 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.assertj - -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.rum.internal.domain.model.ViewEvent -import org.assertj.core.api.AbstractObjectAssert -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset - -internal class ViewEventAssert(actual: ViewEvent) : - AbstractObjectAssert( - actual, - ViewEventAssert::class.java - ) { - - fun hasTimestamp( - expected: Long, - offset: Long = RumEventAssert.TIMESTAMP_THRESHOLD_MS - ): ViewEventAssert { - assertThat(actual.date) - .overridingErrorMessage( - "Expected event to have timestamp $expected but was ${actual.date}" - ) - .isCloseTo(expected, Offset.offset(offset)) - return this - } - - fun hasName(expected: String): ViewEventAssert { - assertThat(actual.view.url) - .overridingErrorMessage( - "Expected event data to have view.url $expected but was ${actual.view.url}" - ) - .isEqualTo(expected) - return this - } - - fun hasDuration( - expected: Long, - offset: Long = DURATION_THRESHOLD_NANOS - ): ViewEventAssert { - assertThat(actual.view.timeSpent) - .overridingErrorMessage( - "Expected event data to have view.time_spent $expected " + - "but was ${actual.view.timeSpent}" - ) - .isEqualTo(expected) - return this - } - - fun hasDurationLowerThan(upperBound: Long): ViewEventAssert { - assertThat(actual.view.timeSpent) - .overridingErrorMessage( - "Expected event data to have view.time_spent lower than $upperBound " + - "but was ${actual.view.timeSpent}" - ) - .isLessThanOrEqualTo(upperBound) - return this - } - - fun hasDurationGreaterThan(upperBound: Long): ViewEventAssert { - assertThat(actual.view.timeSpent) - .overridingErrorMessage( - "Expected event data to have view.time_spent greater than $upperBound " + - "but was ${actual.view.timeSpent}" - ) - .isGreaterThanOrEqualTo(upperBound) - return this - } - - fun hasVersion(expected: Long): ViewEventAssert { - assertThat(actual.dd.documentVersion) - .overridingErrorMessage( - "Expected event data to have dd.documentVersion $expected " + - "but was ${actual.dd.documentVersion}" - ) - .isEqualTo(expected) - return this - } - - fun hasErrorCount(expected: Long): ViewEventAssert { - assertThat(actual.view.error.count) - .overridingErrorMessage( - "Expected event data to have view.error.count $expected " + - "but was ${actual.view.error.count}" - ) - .isEqualTo(expected) - return this - } - - fun hasResourceCount(expected: Long): ViewEventAssert { - assertThat(actual.view.resource.count) - .overridingErrorMessage( - "Expected event data to have view.resource.count $expected " + - "but was ${actual.view.resource.count}" - ) - .isEqualTo(expected) - return this - } - - fun hasActionCount(expected: Long): ViewEventAssert { - assertThat(actual.view.action.count) - .overridingErrorMessage( - "Expected event data to have view.action.count $expected " + - "but was ${actual.view.action.count}" - ) - .isEqualTo(expected) - return this - } - - fun hasCrashCount(expected: Long): ViewEventAssert { - assertThat(actual.view.crash?.count) - .overridingErrorMessage( - "Expected event data to have view.crash.count $expected " + - "but was ${actual.view.crash?.count}" - ) - .isEqualTo(expected) - return this - } - - fun hasViewId(expectedId: String): ViewEventAssert { - assertThat(actual.view.id) - .overridingErrorMessage( - "Expected event data to have view.id $expectedId but was ${actual.view.id}" - ) - .isEqualTo(expectedId) - return this - } - - fun hasApplicationId(expected: String): ViewEventAssert { - assertThat(actual.application.id) - .overridingErrorMessage( - "Expected context to have application.id $expected but was ${actual.application.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasSessionId(expected: String): ViewEventAssert { - assertThat(actual.session.id) - .overridingErrorMessage( - "Expected context to have session.id $expected but was ${actual.session.id}" - ) - .isEqualTo(expected) - return this - } - - fun hasLoadingTime( - expected: Long? - ): ViewEventAssert { - assertThat(actual.view.loadingTime) - .overridingErrorMessage( - "Expected event to have loadingTime $expected" + - " but was ${actual.view.loadingTime}" - ) - .isEqualTo(expected) - return this - } - - fun hasLoadingType( - expected: ViewEvent.LoadingType? - ): ViewEventAssert { - assertThat(actual.view.loadingType) - .overridingErrorMessage( - "Expected event to have loadingType $expected" + - " but was ${actual.view.loadingType}" - ) - .isEqualTo(expected) - return this - } - - fun hasUserInfo(expected: UserInfo?): ViewEventAssert { - assertThat(actual.usr?.id) - .overridingErrorMessage( - "Expected RUM event to have usr.id ${expected?.id} " + - "but was ${actual.usr?.id}" - ) - .isEqualTo(expected?.id) - assertThat(actual.usr?.name) - .overridingErrorMessage( - "Expected RUM event to have usr.name ${expected?.name} " + - "but was ${actual.usr?.name}" - ) - .isEqualTo(expected?.name) - assertThat(actual.usr?.email) - .overridingErrorMessage( - "Expected RUM event to have usr.email ${expected?.email} " + - "but was ${actual.usr?.email}" - ) - .isEqualTo(expected?.email) - return this - } - - companion object { - - internal const val DURATION_THRESHOLD_NANOS = 1000L - - internal fun assertThat(actual: ViewEvent): ViewEventAssert = - ViewEventAssert(actual) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/RumFeatureTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/RumFeatureTest.kt deleted file mode 100644 index c10e18ab2a..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/RumFeatureTest.kt +++ /dev/null @@ -1,558 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal - -import android.app.Application -import com.datadog.android.Datadog -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.data.upload.DataUploadScheduler -import com.datadog.android.core.internal.data.upload.NoOpUploadScheduler -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.privacy.ConsentProvider -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.core.internal.system.SystemInfoProvider -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.plugin.DatadogPlugin -import com.datadog.android.plugin.DatadogPluginConfig -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.domain.RumFileStrategy -import com.datadog.android.rum.internal.monitor.DatadogRumMonitor -import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy -import com.datadog.android.rum.internal.tracking.ViewTreeChangeTrackingStrategy -import com.datadog.android.rum.tracking.ViewTrackingStrategy -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.getFieldValue -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.net.URL -import java.util.concurrent.ExecutorService -import java.util.concurrent.ScheduledThreadPoolExecutor -import okhttp3.OkHttpClient -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumFeatureTest { - - lateinit var mockAppContext: Application - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockSystemInfoProvider: SystemInfoProvider - - @Mock - lateinit var mockOkHttpClient: OkHttpClient - - @Mock - lateinit var mockScheduledThreadPoolExecutor: ScheduledThreadPoolExecutor - - @Mock - lateinit var mockPersistenceExecutorService: ExecutorService - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - lateinit var trackingConsentProvider: ConsentProvider - - lateinit var fakeConfig: DatadogConfig.RumConfig - - lateinit var fakePackageName: String - lateinit var fakePackageVersion: String - - @TempDir - lateinit var tempRootDir: File - - @BeforeEach - fun `set up`(forge: Forge) { - trackingConsentProvider = TrackingConsentProvider() - fakeConfig = DatadogConfig.RumConfig( - clientToken = forge.anHexadecimalString(), - applicationId = forge.getForgery(), - endpointUrl = forge.getForgery().toString(), - envName = forge.anAlphabeticalString() - ) - - fakePackageName = forge.anAlphabeticalString() - fakePackageVersion = forge.aStringMatching("\\d(\\.\\d){3}") - - mockAppContext = mockContext(fakePackageName, fakePackageVersion) - whenever(mockAppContext.filesDir).thenReturn(tempRootDir) - whenever(mockAppContext.applicationContext) doReturn mockAppContext - CoreFeature.isMainProcess = true - } - - @AfterEach - fun `tear down`() { - RumFeature.stop() - CoreFeature.stop() - } - - @Test - fun `initializes GlobalRum context`() { - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - assertThat(RumFeature.applicationId).isEqualTo(fakeConfig.applicationId) - assertThat(RumFeature.endpointUrl).isEqualTo(fakeConfig.endpointUrl) - assertThat(RumFeature.envName).isEqualTo(fakeConfig.envName) - assertThat(RumFeature.clientToken).isEqualTo(fakeConfig.clientToken) - } - - @Test - fun `initializes persistence strategy`() { - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - val persistenceStrategy = RumFeature.persistenceStrategy - assertThat(persistenceStrategy) - .isInstanceOf(RumFileStrategy::class.java) - val reader = RumFeature.persistenceStrategy.getReader() - val suffix: String = reader.getFieldValue("suffix") - assertThat(suffix).isEqualTo("") - val prefix: String = reader.getFieldValue("prefix") - assertThat(prefix).isEqualTo("") - } - - @Test - fun `initializes uploader thread`() { - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - val dataUploadScheduler = RumFeature.dataUploadScheduler - - assertThat(dataUploadScheduler) - .isInstanceOf(DataUploadScheduler::class.java) - } - - @Test - fun `initializes the userInfoProvider`() { - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - assertThat(RumFeature.userInfoProvider).isEqualTo(mockUserInfoProvider) - assertThat(RumFeature.networkInfoProvider).isEqualTo(mockNetworkInfoProvider) - } - - @Test - fun `initializes from configuration`() { - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - val clientToken = RumFeature.clientToken - val endpointUrl = RumFeature.endpointUrl - - assertThat(clientToken).isEqualTo(fakeConfig.clientToken) - assertThat(endpointUrl).isEqualTo(fakeConfig.endpointUrl) - } - - @Test - fun `ignores if initialize called more than once`(forge: Forge) { - Datadog.setVerbosity(android.util.Log.VERBOSE) - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - val persistenceStrategy = RumFeature.persistenceStrategy - val dataUploadScheduler = RumFeature.dataUploadScheduler - val clientToken = RumFeature.clientToken - val endpointUrl = RumFeature.endpointUrl - val userInfoProvider = RumFeature.userInfoProvider - val networkInfoProvider = RumFeature.networkInfoProvider - - fakeConfig = DatadogConfig.RumConfig( - clientToken = forge.anHexadecimalString(), - applicationId = forge.getForgery(), - endpointUrl = forge.getForgery().toString(), - envName = forge.anAlphabeticalString(), - userActionTrackingStrategy = mock(), - viewTrackingStrategy = mock() - ) - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - val persistenceStrategy2 = RumFeature.persistenceStrategy - val dataUploadScheduler2 = RumFeature.dataUploadScheduler - val clientToken2 = RumFeature.clientToken - val endpointUrl2 = RumFeature.endpointUrl - - assertThat(persistenceStrategy).isSameAs(persistenceStrategy2) - assertThat(dataUploadScheduler).isSameAs(dataUploadScheduler2) - assertThat(clientToken).isSameAs(clientToken2) - assertThat(endpointUrl).isSameAs(endpointUrl2) - assertThat(userInfoProvider).isSameAs(RumFeature.userInfoProvider) - assertThat(networkInfoProvider).isSameAs(RumFeature.networkInfoProvider) - } - - @Test - fun `will not register any callback if no instrumentation feature enabled`() { - // When - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - // Then - verify(mockAppContext, never()).registerActivityLifecycleCallbacks( - argThat { - this is ViewTrackingStrategy || this is UserActionTrackingStrategy - } - ) - } - - @Test - fun `will register the strategy when tracking gestures enabled`() { - // Given - val trackGesturesStrategy: UserActionTrackingStrategy = mock() - fakeConfig = fakeConfig.copy(userActionTrackingStrategy = trackGesturesStrategy) - - // When - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - // Then - verify(trackGesturesStrategy).register(mockAppContext) - } - - @Test - fun `will register the strategy when track screen strategy provided`() { - // Given - val viewTrackingStrategy: ViewTrackingStrategy = mock() - fakeConfig = fakeConfig.copy(viewTrackingStrategy = viewTrackingStrategy) - - // When - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - // Then - verify(viewTrackingStrategy).register(mockAppContext) - } - - @Test - fun `will always register the viewTree strategy`() { - // When - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - // Then - verify(mockAppContext).registerActivityLifecycleCallbacks( - argThat { - this is ViewTreeChangeTrackingStrategy - } - ) - } - - @Test - fun `will use a NoOpUploadScheduler if this is not the application main process`() { - // Given - CoreFeature.isMainProcess = false - - // When - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - // Then - assertThat(RumFeature.dataUploadScheduler).isInstanceOf(NoOpUploadScheduler::class.java) - } - - @Test - fun `stops the keep alive callback on stop`() { - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - val monitor: DatadogRumMonitor = mock() - GlobalRum.isRegistered.set(false) - GlobalRum.registerIfAbsent(monitor) - - RumFeature.stop() - - verify(monitor).stopKeepAliveCallback() - } - - @Test - fun `it will register the provided plugin when the feature is initialized`( - forge: Forge - ) { - // Given - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - val mockedTrackingConsentProvider: TrackingConsentProvider = mock() { - whenever(it.getConsent()).thenReturn(fakeConsent) - } - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - - // When - RumFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - mockedTrackingConsentProvider - ) - - val argumentCaptor = argumentCaptor() - - // Then - val mockPlugins = plugins.toTypedArray() - inOrder(*mockPlugins) { - mockPlugins.forEach { - verify(it).register(argumentCaptor.capture()) - } - } - - argumentCaptor.allValues.forEach { - assertThat(it).isInstanceOf(DatadogPluginConfig.RumPluginConfig::class.java) - assertThat(it.context).isEqualTo(mockAppContext) - assertThat(it.serviceName).isEqualTo(CoreFeature.serviceName) - assertThat(it.envName).isEqualTo(fakeConfig.envName) - assertThat(it.featurePersistenceDirName).isEqualTo(RumFileStrategy.AUTHORIZED_FOLDER) - assertThat(it.context).isEqualTo(mockAppContext) - assertThat(it.trackingConsent).isEqualTo(fakeConsent) - } - } - - @Test - fun `M register the plugins as TrackingConsentProvideCallback W initialized`( - forge: Forge - ) { - // Given - val fakeConsent = forge.aValueFrom(TrackingConsent::class.java) - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - val mockedTrackingConsentProvider: TrackingConsentProvider = mock() { - whenever(it.getConsent()).thenReturn(fakeConsent) - } - - // When - RumFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - mockedTrackingConsentProvider - ) - - // Then - val mockPlugins = plugins.toTypedArray() - mockPlugins.forEach { - verify(mockedTrackingConsentProvider).registerCallback(it) - } - } - - @Test - fun `it will unregister the provided plugin when stop called`( - forge: Forge - ) { - // Given - val plugins: List = forge.aList(forge.anInt(min = 1, max = 10)) { - mock() - } - RumFeature.initialize( - mockAppContext, - fakeConfig.copy(plugins = plugins), - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - - // When - RumFeature.stop() - - // Then - val mockPlugins = plugins.toTypedArray() - inOrder(*mockPlugins) { - mockPlugins.forEach { - verify(it).unregister() - } - } - } - - @Test - fun `clears all files on local storage on request`( - @StringForgery(type = StringForgeryType.NUMERICAL) fileName: String, - @StringForgery content: String - ) { - val fakeDir = File(tempRootDir, RumFileStrategy.AUTHORIZED_FOLDER) - fakeDir.mkdirs() - val fakeFile = File(fakeDir, fileName) - fakeFile.writeText(content) - - // When - RumFeature.initialize( - mockAppContext, - fakeConfig, - mockOkHttpClient, - mockNetworkInfoProvider, - mockSystemInfoProvider, - mockScheduledThreadPoolExecutor, - mockPersistenceExecutorService, - mockUserInfoProvider, - trackingConsentProvider - ) - RumFeature.clearAllData() - - // Then - assertThat(fakeFile).doesNotExist() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/RumEventSerializerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/RumEventSerializerTest.kt deleted file mode 100644 index 7c61d53398..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/RumEventSerializerTest.kt +++ /dev/null @@ -1,521 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain - -import com.datadog.android.core.internal.constraints.DataConstraints -import com.datadog.android.core.internal.utils.toJsonArray -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.event.RumEventSerializer -import com.datadog.android.rum.internal.domain.model.ActionEvent -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.model.ResourceEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.assertj.JsonObjectAssert.Companion.assertThat -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Date -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumEventSerializerTest { - - lateinit var testedSerializer: RumEventSerializer - - @BeforeEach - fun `set up`() { - testedSerializer = RumEventSerializer() - } - - @Test - fun `𝕄 serialize RUM event 𝕎 serialize() with ResourceEvent`( - @Forgery fakeEvent: RumEvent, - @Forgery event: ResourceEvent - ) { - val rumEvent = fakeEvent.copy(event = event) - - val serialized = testedSerializer.serialize(rumEvent) - - val jsonObject = JsonParser.parseString(serialized).asJsonObject - assertSerializedJsonMatchesInputEvent(jsonObject, rumEvent) - assertThat(jsonObject) - .hasField("type", "resource") - .hasField("date", event.date) - .hasField("resource") { - hasField("type", event.resource.type.name.toLowerCase()) - hasField("url", event.resource.url) - hasField("duration", event.resource.duration) - hasNullableField("method", event.resource.method?.name) - hasNullableField("status_code", event.resource.statusCode) - hasNullableField("size", event.resource.size) - // TODO timing ? - } - .hasField("application") { - hasField("id", event.application.id) - } - .hasField("session") { - hasField("id", event.session.id) - hasField("type", event.session.type.name.toLowerCase()) - } - .hasField("view") { - hasField("id", event.view.id) - hasField("url", event.view.url) - } - .hasField("usr") { - hasNullableField("id", event.usr?.id) - hasNullableField("name", event.usr?.name) - hasNullableField("email", event.usr?.email) - } - .hasField("_dd") { - hasField("format_version", 2L) - } - } - - @Test - fun `𝕄 serialize RUM event 𝕎 serialize() with ActionEvent`( - @Forgery fakeEvent: RumEvent, - @Forgery event: ActionEvent - ) { - val rumEvent = fakeEvent.copy(event = event) - - val serialized = testedSerializer.serialize(rumEvent) - - val jsonObject = JsonParser.parseString(serialized).asJsonObject - assertSerializedJsonMatchesInputEvent(jsonObject, rumEvent) - assertThat(jsonObject) - .hasField("type", "action") - .hasField("date", event.date) - .hasField("action") { - hasField("type", event.action.type.name.toLowerCase()) - hasNullableField("id", event.action.id) - event.action.target?.let { - hasField("target") { - hasField("name", it.name) - } - } - event.action.resource?.let { - hasField("resource") { - hasField("count", it.count) - } - } - event.action.error?.let { - hasField("error") { - hasField("count", it.count) - } - } - event.action.longTask?.let { - hasField("long_task") { - hasField("count", it.count) - } - } - hasNullableField("loading_time", event.action.loadingTime) - } - .hasField("application") { - hasField("id", event.application.id) - } - .hasField("session") { - hasField("id", event.session.id) - hasField("type", event.session.type.name.toLowerCase()) - } - .hasField("view") { - hasField("id", event.view.id) - hasField("url", event.view.url) - } - .hasField("usr") { - hasNullableField("id", event.usr?.id) - hasNullableField("name", event.usr?.name) - hasNullableField("email", event.usr?.email) - } - .hasField("_dd") { - hasField("format_version", 2L) - } - } - - @Test - fun `𝕄 serialize RUM event 𝕎 serialize() with ViewEvent`( - @Forgery fakeEvent: RumEvent, - @Forgery event: ViewEvent - ) { - val rumEvent = fakeEvent.copy(event = event) - - val serialized = testedSerializer.serialize(rumEvent) - - val jsonObject = JsonParser.parseString(serialized).asJsonObject - assertSerializedJsonMatchesInputEvent(jsonObject, rumEvent) - assertThat(jsonObject) - .hasField("type", "view") - .hasField("date", event.date) - .hasField("application") { - hasField("id", event.application.id) - } - .hasField("session") { - hasField("id", event.session.id) - hasField("type", event.session.type.name.toLowerCase()) - } - .hasField("view") { - hasField("id", event.view.id) - hasField("url", event.view.url) - hasField("time_spent", event.view.timeSpent) - hasField("action") { - hasField("count", event.view.action.count) - } - hasField("resource") { - hasField("count", event.view.resource.count) - } - hasField("error") { - hasField("count", event.view.error.count) - } - event.view.longTask?.let { - hasField("long_task") { - hasField("count", it.count) - } - } - } - .hasField("usr") { - hasNullableField("id", event.usr?.id) - hasNullableField("name", event.usr?.name) - hasNullableField("email", event.usr?.email) - } - .hasField("_dd") { - hasField("format_version", 2L) - } - } - - @Test - fun `𝕄 serialize RUM event 𝕎 serialize() with ErrorEvent`( - @Forgery fakeEvent: RumEvent, - @Forgery event: ErrorEvent - ) { - val rumEvent = fakeEvent.copy(event = event) - - val serialized = testedSerializer.serialize(rumEvent) - - val jsonObject = JsonParser.parseString(serialized).asJsonObject - assertSerializedJsonMatchesInputEvent(jsonObject, rumEvent) - assertThat(jsonObject) - .hasField("type", "error") - .hasField("date", event.date) - .hasField("error") { - hasField("message", event.error.message) - hasField("source", event.error.source.name.toLowerCase()) - hasNullableField("stack", event.error.stack) - event.error.resource?.let { - hasField("resource") { - hasField("method", it.method.name.toUpperCase()) - hasField("status_code", it.statusCode) - hasField("url", it.url) - } - } - } - .hasField("application") { - hasField("id", event.application.id) - } - .hasField("session") { - hasField("id", event.session.id) - hasField("type", event.session.type.name.toLowerCase()) - } - .hasField("view") { - hasField("id", event.view.id) - hasField("url", event.view.url) - } - .hasField("usr") { - hasNullableField("id", event.usr?.id) - hasNullableField("name", event.usr?.name) - hasNullableField("email", event.usr?.email) - } - .hasField("_dd") { - hasField("format_version", 2L) - } - } - - @Test - fun `𝕄 serialize RUM event 𝕎 serialize() with unknown event`( - @Forgery fakeEvent: RumEvent, - @Forgery unknownEvent: UserInfo - ) { - val rumEvent = fakeEvent.copy(event = unknownEvent) - - val serialized = testedSerializer.serialize(rumEvent) - - val jsonObject = JsonParser.parseString(serialized).asJsonObject - assertSerializedJsonMatchesInputEvent(jsonObject, rumEvent) - assertThat(jsonObject) - .doesNotHaveField("type") - .doesNotHaveField("date") - .doesNotHaveField("error") - .doesNotHaveField("action") - .doesNotHaveField("resource") - .doesNotHaveField("application") - .doesNotHaveField("session") - .doesNotHaveField("view") - .doesNotHaveField("usr") - .doesNotHaveField("_dd") - } - - @Test - fun `𝕄 keep known custom attributes as is 𝕎 serialize()`( - @Forgery fakeEvent: RumEvent, - forge: Forge - ) { - val key = forge.anElementFrom(RumEventSerializer.knownAttributes) - val value = forge.anAlphabeticalString() - val event = fakeEvent.copy(globalAttributes = mapOf(key to value)) - - val serialized = testedSerializer.serialize(event) - - val jsonObject = JsonParser.parseString(serialized).asJsonObject - - assertThat(jsonObject) - .hasField(key, value) - } - - @Test - fun `M serialize W serialize() with custom timing`( - forge: Forge - ) { - // GIVEN - val fakeCustomTimings = forge.aMap { forge.anAlphabeticalString() to forge.aLong() } - val fakeEvent: RumEvent = - forge.getForgery(RumEvent::class.java).copy(customTimings = fakeCustomTimings) - - // WHEN - val serialized = testedSerializer.serialize(fakeEvent) - val jsonObject = JsonParser.parseString(serialized).asJsonObject - - // THEN - fakeCustomTimings - .filter { it.key.isNotBlank() } - .forEach { - val keyName = "${RumEventSerializer.VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX}.${it.key}" - assertThat(jsonObject).hasField(keyName, it.value) - } - } - - @Test - fun `M not add custom timings group at all W serialize() with custom timings null`( - @Forgery fakeEvent: RumEvent - ) { - // WHEN - val serialized = testedSerializer.serialize(fakeEvent) - val jsonObject = JsonParser.parseString(serialized).asJsonObject - - // THEN - assertThat(jsonObject) - .doesNotHaveField(RumEventSerializer.VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX) - } - - @Test - fun `M sanitise the custom attributes keys W level deeper than 9`(forge: Forge) { - // GIVEN - val fakeBadKey = - forge.aList(size = 10) { forge.anAlphabeticalString() }.joinToString(".") - val lastIndexOf = fakeBadKey.lastIndexOf('.') - val expectedSanitisedKey = - fakeBadKey.replaceRange(lastIndexOf..lastIndexOf, "_") - val fakeAttributeValue = forge.anAlphabeticalString() - val fakeEvent: RumEvent = forge.getForgery(RumEvent::class.java).copy( - globalAttributes = mapOf( - fakeBadKey to fakeAttributeValue - ) - ) - - // WHEN - val serializedEvent = testedSerializer.serialize(fakeEvent) - val jsonObject = JsonParser.parseString(serializedEvent).asJsonObject - - // THEN - assertThat(jsonObject) - .hasField( - "${RumEventSerializer.GLOBAL_ATTRIBUTE_PREFIX}.$expectedSanitisedKey", - fakeAttributeValue - ) - assertThat(jsonObject) - .doesNotHaveField("${RumEventSerializer.GLOBAL_ATTRIBUTE_PREFIX}.$fakeBadKey") - } - - @Test - fun `M sanitise the user extra info keys W level deeper than 8`(forge: Forge) { - // GIVEN - val fakeBadKey = - forge.aList(size = 9) { forge.anAlphabeticalString() }.joinToString(".") - val lastIndexOf = fakeBadKey.lastIndexOf('.') - val expectedSanitisedKey = - fakeBadKey.replaceRange(lastIndexOf..lastIndexOf, "_") - val fakeAttributeValue = forge.anAlphabeticalString() - val fakeEvent: RumEvent = forge.getForgery(RumEvent::class.java).copy( - userExtraAttributes = mapOf( - fakeBadKey to fakeAttributeValue - ) - ) - - // WHEN - val serializedEvent = testedSerializer.serialize(fakeEvent) - val jsonObject = JsonParser.parseString(serializedEvent).asJsonObject - - // THEN - assertThat(jsonObject) - .hasField( - "${RumEventSerializer.USER_ATTRIBUTE_PREFIX}.$expectedSanitisedKey", - fakeAttributeValue - ) - assertThat(jsonObject) - .doesNotHaveField("${RumEventSerializer.USER_ATTRIBUTE_PREFIX}.$fakeBadKey") - } - - @Test - fun `M sanitise the custom timings keys W level deeper than 8`(forge: Forge) { - // GIVEN - val fakeBadKey = - forge.aList(size = 9) { forge.anAlphabeticalString() }.joinToString(".") - val lastIndexOf = fakeBadKey.lastIndexOf('.') - val expectedSanitisedKey = - fakeBadKey.replaceRange(lastIndexOf..lastIndexOf, "_") - val fakeTimingValue = forge.aLong(min = 1) - val fakeEvent: RumEvent = forge.getForgery(RumEvent::class.java).copy( - customTimings = mapOf( - fakeBadKey to fakeTimingValue - ) - ) - - // WHEN - val serializedEvent = testedSerializer.serialize(fakeEvent) - val jsonObject = JsonParser.parseString(serializedEvent).asJsonObject - - // THEN - assertThat(jsonObject) - .hasField( - "${RumEventSerializer.VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX}.$expectedSanitisedKey", - fakeTimingValue - ) - assertThat(jsonObject) - .doesNotHaveField( - "${RumEventSerializer.VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX}.$fakeBadKey" - ) - } - - @Test - fun `M use the attributes group verbose name W validateAttributes { user extra info }`( - @Forgery fakeEvent: RumEvent - ) { - - // GIVEN - val mockedDataConstrains: DataConstraints = mock() - testedSerializer = RumEventSerializer(mockedDataConstrains) - - // WHEN - testedSerializer.serialize(fakeEvent) - - // THEN - verify(mockedDataConstrains).validateAttributes( - fakeEvent.userExtraAttributes, - RumEventSerializer.USER_ATTRIBUTE_PREFIX, - RumEventSerializer.USER_EXTRA_GROUP_VERBOSE_NAME - ) - } - - @Test - fun `M use the attributes group verbose name W validateAttributes { custom timings }`( - forge: Forge - ) { - - // GIVEN - val mockedDataConstrains: DataConstraints = mock() - testedSerializer = RumEventSerializer(mockedDataConstrains) - val fakeCustomTimings = forge.aMap { forge.anAlphabeticalString() to forge.aLong(min = 1) } - val fakeEvent: RumEvent = forge.getForgery(RumEvent::class.java).copy( - customTimings = fakeCustomTimings - ) - - // WHEN - testedSerializer.serialize(fakeEvent) - - // THEN - verify(mockedDataConstrains).validateAttributes( - fakeCustomTimings, - RumEventSerializer.VIEW_CUSTOM_TIMINGS_ATTRIBUTE_PREFIX, - RumEventSerializer.CUSTOM_TIMINGS_GROUP_VERBOSE_NAME - ) - } - - // region Internal - - private fun assertSerializedJsonMatchesInputEvent( - jsonObject: JsonObject, - event: RumEvent - ) { - assertJsonContainsCustomAttributes(jsonObject, event) - } - - private fun assertJsonContainsCustomAttributes( - jsonObject: JsonObject, - event: RumEvent - ) { - event.globalAttributes - .filter { it.key.isNotBlank() } - .forEach { - val value = it.value - val keyName = "${RumEventSerializer.GLOBAL_ATTRIBUTE_PREFIX}.${it.key}" - when (value) { - null -> assertThat(jsonObject).hasNullField(keyName) - is Boolean -> assertThat(jsonObject).hasField(keyName, value) - is Int -> assertThat(jsonObject).hasField(keyName, value) - is Long -> assertThat(jsonObject).hasField(keyName, value) - is Float -> assertThat(jsonObject).hasField(keyName, value) - is Double -> assertThat(jsonObject).hasField(keyName, value) - is String -> assertThat(jsonObject).hasField(keyName, value) - is Date -> assertThat(jsonObject).hasField(keyName, value.time) - is JsonObject -> assertThat(jsonObject).hasField(keyName, value) - is JsonArray -> assertThat(jsonObject).hasField(keyName, value) - is Iterable<*> -> assertThat(jsonObject).hasField(keyName, value.toJsonArray()) - else -> assertThat(jsonObject).hasField(keyName, value.toString()) - } - } - event.userExtraAttributes - .filter { it.key.isNotBlank() } - .forEach { - val value = it.value - val keyName = "${RumEventSerializer.USER_ATTRIBUTE_PREFIX}.${it.key}" - when (value) { - null -> assertThat(jsonObject).hasNullField(keyName) - is Boolean -> assertThat(jsonObject).hasField(keyName, value) - is Int -> assertThat(jsonObject).hasField(keyName, value) - is Long -> assertThat(jsonObject).hasField(keyName, value) - is Float -> assertThat(jsonObject).hasField(keyName, value) - is Double -> assertThat(jsonObject).hasField(keyName, value) - is String -> assertThat(jsonObject).hasField(keyName, value) - is Date -> assertThat(jsonObject).hasField(keyName, value.time) - is JsonObject -> assertThat(jsonObject).hasField(keyName, value) - is JsonArray -> assertThat(jsonObject).hasField(keyName, value) - is Iterable<*> -> assertThat(jsonObject).hasField(keyName, value.toJsonArray()) - else -> assertThat(jsonObject).hasField(keyName, value.toString()) - } - } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/RumFileStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/RumFileStrategyTest.kt deleted file mode 100644 index 9e0eed2429..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/RumFileStrategyTest.kt +++ /dev/null @@ -1,74 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain - -import android.content.Context -import com.datadog.android.core.internal.domain.FilePersistenceConfig -import com.datadog.android.core.internal.domain.assertj.PersistenceStrategyAssert -import com.datadog.android.core.internal.privacy.TrackingConsentProvider -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.io.File -import java.util.concurrent.ExecutorService -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class RumFileStrategyTest { - lateinit var testedStrategy: RumFileStrategy - - lateinit var mockedContext: Context - - @Mock - lateinit var mockExecutorService: ExecutorService - - lateinit var trackingConsentProvider: TrackingConsentProvider - - @BeforeEach - fun `set up`() { - mockedContext = mockContext() - trackingConsentProvider = TrackingConsentProvider() - testedStrategy = RumFileStrategy( - mockedContext, - dataPersistenceExecutorService = mockExecutorService, - trackingConsentProvider = trackingConsentProvider - ) - } - - @Test - fun `M correctly initialise the strategy W instantiated`() { - val absolutePath = mockedContext.filesDir.absolutePath - val expectedIntermediateFolderPath = - absolutePath + - File.separator + - RumFileStrategy.INTERMEDIATE_DATA_FOLDER - val expectedAuthorizedFolderPath = - absolutePath + - File.separator + - RumFileStrategy.AUTHORIZED_FOLDER - PersistenceStrategyAssert - .assertThat(testedStrategy) - .hasIntermediateStorageFolder(expectedIntermediateFolderPath) - .hasAuthorizedStorageFolder(expectedAuthorizedFolderPath) - .uploadsFrom(expectedAuthorizedFolderPath) - .usesConsentAwareAsyncWriter() - .hasConfig(FilePersistenceConfig(RumFileStrategy.MAX_DELAY_BETWEEN_RUM_EVENTS_MS)) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt deleted file mode 100644 index 6399cac460..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt +++ /dev/null @@ -1,1098 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.log.internal.user.NoOpMutableUserInfoProvider -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.assertj.RumEventAssert.Companion.assertThat -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.forge.exhaustiveAttributes -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.same -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.TimeUnit -import kotlin.system.measureNanoTime -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumActionScopeTest { - - lateinit var testedScope: RumActionScope - - @Mock - lateinit var mockParentScope: RumScope - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @Mock - lateinit var mockWriter: Writer - - @Forgery - lateinit var fakeType: RumActionType - - @StringForgery - lateinit var fakeName: String - - lateinit var fakeKey: ByteArray - lateinit var fakeAttributes: Map - - @Forgery - lateinit var fakeParentContext: RumContext - - @Forgery - lateinit var fakeUserInfo: UserInfo - - lateinit var fakeEventTime: Time - - lateinit var fakeEvent: RumRawEvent - - @BeforeEach - fun `set up`(forge: Forge) { - fakeEventTime = Time() - - RumFeature::class.java.setStaticValue("userInfoProvider", mockUserInfoProvider) - - fakeAttributes = forge.exhaustiveAttributes() - fakeKey = forge.anAsciiString().toByteArray() - - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - whenever(mockParentScope.getRumContext()) doReturn fakeParentContext - - testedScope = RumActionScope( - mockParentScope, - false, - fakeEventTime, - fakeType, - fakeName, - fakeAttributes - ) - } - - @AfterEach - fun `tear down`() { - RumFeature::class.java.setStaticValue("userInfoProvider", NoOpMutableUserInfoProvider()) - GlobalRum.globalAttributes.clear() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+StopResource+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @Forgery kind: RumResourceKind - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - fakeEvent = RumRawEvent.StopResource(key, statusCode, size, kind, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(1000)) - hasResourceCount(1) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isNull() - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StartResource+StopResource+any) {unknown key}`( - @StringForgery key: String, - @StringForgery key2: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @Forgery kind: RumResourceKind - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - fakeEvent = RumRawEvent.StopResource(key2, statusCode, size, kind, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockParentScope, mockWriter) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+StopResourceWithError+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - @LongForgery(200, 600) statusCode: Long, - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - fakeEvent = RumRawEvent.StopResourceWithError(key, statusCode, message, source, throwable) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(1000)) - hasResourceCount(0) - hasErrorCount(1) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isNull() - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StartResource+StopResourceWithError+any) {unknown key}`( - @StringForgery key: String, - @StringForgery key2: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - @LongForgery(200, 600) statusCode: Long, - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - fakeEvent = RumRawEvent.StopResourceWithError(key2, statusCode, message, source, throwable) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockParentScope, mockWriter) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+any) missing resource key`( - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // Given - var key: Any? = Object() - - // When - fakeEvent = RumRawEvent.StartResource(key.toString(), url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - key = null - fakeEvent = mockEvent() - System.gc() - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasResourceCount(1) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(AddError+any)`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - false, - emptyMap() - ) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(100)) - hasResourceCount(0) - hasErrorCount(1) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(AddError) {isFatal=true}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - true, - emptyMap() - ) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(100)) - hasResourceCount(0) - hasErrorCount(1) - hasCrashCount(1) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(AddError{isFatal=false}+AddError{isFatal=true})`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.AddError(message, source, throwable, null, false, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - fakeEvent = RumRawEvent.AddError(message, source, throwable, null, true, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(100)) - hasResourceCount(0) - hasErrorCount(2) - hasCrashCount(1) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {viewTreeChangeCount != 0}`( - @IntForgery(1) count: Int - ) { - // When - testedScope.viewTreeChangeCount = count - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {resourceCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.resourceCount = count - - // When - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(count) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {errorCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.errorCount = count - - // When - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(count) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {crashCount != 0}`( - @LongForgery(1, 1024) nonFatalcount: Long, - @LongForgery(1, 1024) fatalcount: Long - ) { - // Given - testedScope.errorCount = nonFatalcount + fatalcount - testedScope.crashCount = fatalcount - - // When - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(nonFatalcount + fatalcount) - hasCrashCount(fatalcount) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(any) {viewTreeChangeCount != 0}`( - @IntForgery(1) count: Int - ) { - // Given - testedScope.viewTreeChangeCount = count - - // When - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action with global attributes after threshold 𝕎 handleEvent(any)`( - @IntForgery(1) count: Int, - forge: Forge - ) { - // Given - val attributes = forge.aMap { anHexadecimalString() to anAsciiString() } - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - testedScope.viewTreeChangeCount = count - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - GlobalRum.globalAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event with user extra attributes 𝕎 handleEvent(any)`( - @IntForgery(1) count: Int, - forge: Forge - ) { - // Given - testedScope.viewTreeChangeCount = count - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(any) {resourceCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.resourceCount = count - - // When - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(count) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(any) {errorCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.errorCount = count - - // When - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(count) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(any) {crashCount != 0}`( - @LongForgery(1, 1024) nonFatalcount: Long, - @LongForgery(1, 1024) fatalcount: Long - ) { - // Given - testedScope.errorCount = nonFatalcount + fatalcount - testedScope.crashCount = fatalcount - - // When - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasErrorCount(nonFatalcount + fatalcount) - hasCrashCount(fatalcount) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action only once 𝕎 handleEvent(any) twice`( - @IntForgery(1, 1024) count: Int - ) { - // Given - testedScope.viewTreeChangeCount = count - - // When - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result = testedScope.handleEvent(mockEvent(), mockWriter) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - assertThat(result2).isNull() - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(StopView) {no side effect}`() { - // Given - testedScope.resourceCount = 0 - testedScope.viewTreeChangeCount = 0 - testedScope.errorCount = 0 - testedScope.crashCount = 0 - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isNull() - } - - @Test - fun `𝕄 doNothing after threshold 𝕎 handleEvent(any) {no side effect}`() { - // Given - testedScope.resourceCount = 0 - testedScope.viewTreeChangeCount = 0 - testedScope.errorCount = 0 - testedScope.crashCount = 0 - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - fakeEvent = mockEvent() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isNull() - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(any) before threshold`() { - // Given - testedScope.viewTreeChangeCount = 1 - fakeEvent = mockEvent() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(StartResource+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(1000) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Action after timeout 𝕎 handleEvent(StartResource+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_MAX_DURATION_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(RumActionScope.ACTION_MAX_DURATION_NS) - hasResourceCount(1) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action after timeout 𝕎 handleEvent(any)`() { - // When - testedScope.viewTreeChangeCount = 1 - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(ViewTreeChanged+any)`() { - // When - val duration = measureNanoTime { - repeat(10) { - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS / 3) - testedScope.handleEvent(RumRawEvent.ViewTreeChanged(Time()), mockWriter) - } - } - testedScope.handleEvent(RumRawEvent.ViewTreeChanged(Time()), mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(duration) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - // region Internal - - private fun mockEvent(): RumRawEvent { - val event: RumRawEvent = mock() - whenever(event.eventTime) doReturn Time() - return event - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt deleted file mode 100644 index 305e00c21e..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt +++ /dev/null @@ -1,94 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.utils.forge.Configurator -import com.datadog.tools.unit.setFieldValue -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.annotation.FloatForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.UUID -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumApplicationScopeTest { - - lateinit var testedScope: RumApplicationScope - - @Mock - lateinit var mockChildScope: RumScope - - @Mock - lateinit var mockEvent: RumRawEvent - - @Mock - lateinit var mockWriter: Writer - - @Mock - lateinit var mockDetector: FirstPartyHostDetector - - @Forgery - lateinit var fakeApplicationId: UUID - - @FloatForgery(min = 0f, max = 100f) - var fakeSamplingRate: Float = 0f - - @BeforeEach - fun `set up`() { - testedScope = RumApplicationScope(fakeApplicationId, fakeSamplingRate, mockDetector) - } - - @AfterEach - fun `tear down`() { - } - - @Test - fun `create child session scope with sampling rate`() { - val childScope = testedScope.childScope - - check(childScope is RumSessionScope) - assertThat(childScope.samplingRate).isEqualTo(fakeSamplingRate) - assertThat(childScope.firstPartyHostDetector).isSameAs(mockDetector) - } - - @Test - fun `always returns the same applicationId`() { - val context = testedScope.getRumContext() - - assertThat(context.applicationId).isEqualTo(fakeApplicationId.toString()) - } - - @Test - fun `delegates all events to child scope`() { - testedScope.setFieldValue("childScope", mockChildScope) - - testedScope.handleEvent(mockEvent, mockWriter) - - verify(mockChildScope).handleEvent(mockEvent, mockWriter) - verifyZeroInteractions(mockWriter) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt deleted file mode 100644 index dd73e875a6..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt +++ /dev/null @@ -1,1223 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.time.TimeProvider -import com.datadog.android.log.internal.user.NoOpMutableUserInfoProvider -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.assertj.RumEventAssert.Companion.assertThat -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.forge.exhaustiveAttributes -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.same -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.TimeUnit -import kotlin.system.measureNanoTime -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assumptions.assumeTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumContinuousActionScopeTest { - - lateinit var testedScope: RumActionScope - - @Mock - lateinit var mockParentScope: RumScope - - @Mock - lateinit var mockTimeProvider: TimeProvider - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @Mock - lateinit var mockWriter: Writer - - @Forgery - lateinit var fakeType: RumActionType - - @StringForgery - lateinit var fakeName: String - - lateinit var fakeKey: ByteArray - lateinit var fakeAttributes: Map - - @Forgery - lateinit var fakeParentContext: RumContext - - @Forgery - lateinit var fakeUserInfo: UserInfo - - lateinit var fakeEventTime: Time - - lateinit var fakeEvent: RumRawEvent - - @BeforeEach - fun `set up`(forge: Forge) { - fakeEventTime = Time() - - RumFeature::class.java.setStaticValue("userInfoProvider", mockUserInfoProvider) - - fakeAttributes = forge.exhaustiveAttributes() - fakeKey = forge.anAsciiString().toByteArray() - - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - whenever(mockParentScope.getRumContext()) doReturn fakeParentContext - - testedScope = RumActionScope( - mockParentScope, - true, - fakeEventTime, - fakeType, - fakeName, - fakeAttributes - ) - } - - @AfterEach - fun `tear down`() { - RumFeature::class.java.setStaticValue("userInfoProvider", NoOpMutableUserInfoProvider()) - GlobalRum.globalAttributes.clear() - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(any) {viewTreeChangeCount != 0}`( - @IntForgery(1) count: Int - ) { - // Given - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - testedScope.viewTreeChangeCount = count - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(any) {resourceCount != 0}`( - @LongForgery(1) count: Long - ) { - // Given - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - testedScope.resourceCount = count - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(any) {errorCount != 0}`( - @LongForgery(1) count: Long - ) { - // Given - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - testedScope.errorCount = count - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(any) {crashCount != 0}`( - @LongForgery(1) nonFatalcount: Long, - @LongForgery(1) fatalcount: Long - ) { - // Given - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - testedScope.errorCount = nonFatalcount + fatalcount - testedScope.crashCount = fatalcount - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Action after timeout 𝕎 handleEvent(any)`() { - // Given - testedScope.viewTreeChangeCount = 1 - Thread.sleep(RumActionScope.ACTION_MAX_DURATION_MS) - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(RumActionScope.ACTION_MAX_DURATION_NS) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action with updated data 𝕎 handleEvent(StopAction+any) {viewTreeChangeCount!= 0)`( - @Forgery type: RumActionType, - @StringForgery name: String, - forge: Forge - ) { - // Given - assumeTrue { type != fakeType } - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - testedScope.viewTreeChangeCount = 1 - - // When - Thread.sleep(500) - fakeEvent = RumRawEvent.StopAction(type, name, attributes) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasTargetName(name) - hasType(type) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+StopAction+StopResource+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @Forgery kind: RumResourceKind - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(1000) - fakeEvent = RumRawEvent.StopResource(key, statusCode, size, kind, emptyMap()) - val result3 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result4 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(1000)) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(1500)) - hasResourceCount(1) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isSameAs(testedScope) - assertThat(result4).isNull() - } - - @Test - fun `𝕄 send Action 𝕎 handleEvent(StartResource+StopAction+StopResourceWithError+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - @LongForgery(200, 600) statusCode: Long, - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(1000) - fakeEvent = RumRawEvent.StopResourceWithError(key, statusCode, message, source, throwable) - val result3 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result4 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(100)) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(1500)) - hasResourceCount(0) - hasErrorCount(1) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isSameAs(testedScope) - assertThat(result4).isNull() - } - - @Test - fun `𝕄 send Action 𝕎 handleEvent(StartResource+StopAction+any) missing resource key`( - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // Given - var key: Any? = Object() - - // When - fakeEvent = RumRawEvent.StartResource(key.toString(), url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(1000) - key = null - fakeEvent = mockEvent() - System.gc() - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(1000)) - hasResourceCount(1) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(AddError+StopAction+any)`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - false, - emptyMap() - ) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(1000) - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(1000)) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(1500)) - hasResourceCount(0) - hasErrorCount(1) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(AddError) {isFatal=true}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - true, - emptyMap() - ) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(100)) - hasResourceCount(0) - hasErrorCount(1) - hasCrashCount(1) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(AddError{isFatal=false}+AddError{isFatal=true})`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - // When - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - false, - emptyMap() - ) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - true, - emptyMap() - ) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationLowerThan(TimeUnit.MILLISECONDS.toNanos(100)) - hasResourceCount(0) - hasErrorCount(2) - hasCrashCount(1) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {viewTreeChangeCount != 0}`( - @IntForgery(1) count: Int - ) { - // Given - testedScope.viewTreeChangeCount = count - - // When - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {resourceCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.resourceCount = count - - // When - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(count) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {errorCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.errorCount = count - - // When - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(count) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action immediately 𝕎 handleEvent(StopView) {crashCount != 0}`( - @LongForgery(1, 1024) nonFatalcount: Long, - @LongForgery(1, 1024) fatalcount: Long - ) { - // Given - testedScope.errorCount = nonFatalcount + fatalcount - testedScope.crashCount = fatalcount - - // When - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(nonFatalcount + fatalcount) - hasCrashCount(fatalcount) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StopAction+any) {viewTreeChangeCount != 0}`( - @IntForgery(1) count: Int - ) { - // Given - testedScope.viewTreeChangeCount = count - - // When - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action with global attributes after threshold 𝕎 handleEvent(StopAction+any)`( - @IntForgery(1) count: Int, - forge: Forge - ) { - // Given - val attributes = forge.aMap { anHexadecimalString() to anAsciiString() } - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - testedScope.viewTreeChangeCount = count - GlobalRum.globalAttributes.putAll(attributes) - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StopAction+any) {resourceCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.resourceCount = count - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(count) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StopAction+any) {errorCount != 0}`( - @LongForgery(1, 1024) count: Long - ) { - // Given - testedScope.errorCount = count - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(count) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StopAction+any) {crashCount != 0}`( - @LongForgery(1, 1024) nonFatalcount: Long, - @LongForgery(1, 1024) fatalcount: Long - ) { - // Given - testedScope.errorCount = nonFatalcount + fatalcount - testedScope.crashCount = fatalcount - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasErrorCount(nonFatalcount + fatalcount) - hasCrashCount(fatalcount) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action only once 𝕎 handleEvent(StopAction) + handleEvent(any) twice`( - @IntForgery(1, 1024) count: Int - ) { - // Given - testedScope.viewTreeChangeCount = count - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(1) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - assertThat(result3).isNull() - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(StopView) {no side effect}`() { - // Given - testedScope.resourceCount = 0 - testedScope.viewTreeChangeCount = 0 - testedScope.errorCount = 0 - testedScope.crashCount = 0 - fakeEvent = RumRawEvent.StopView(Object(), emptyMap()) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isNull() - } - - @Test - fun `𝕄 doNothing after threshold 𝕎 handleEvent(any) {no side effect}`() { - // Given - testedScope.resourceCount = 0 - testedScope.viewTreeChangeCount = 0 - testedScope.errorCount = 0 - testedScope.crashCount = 0 - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(any) before threshold`() { - // Given - testedScope.viewTreeChangeCount = 1 - - // When - val result = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(StartResource+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(1000) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - } - - @Test - fun `𝕄 doNothing 𝕎 handleEvent(StartResource+StopAction+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - verifyZeroInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Action after timeout 𝕎 handleEvent(StartResource+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_MAX_DURATION_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(RumActionScope.ACTION_MAX_DURATION_NS) - hasResourceCount(1) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send Action after timeout 𝕎 handleEvent(StartResource+StopAction+any)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - // When - fakeEvent = RumRawEvent.StartResource(key, url, method, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(500) - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result2 = testedScope.handleEvent(fakeEvent, mockWriter) - Thread.sleep(RumActionScope.ACTION_MAX_DURATION_MS) - val result3 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(RumActionScope.ACTION_MAX_DURATION_NS) - hasResourceCount(1) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isSameAs(testedScope) - assertThat(result3).isNull() - } - - @Test - fun `𝕄 send Action after threshold 𝕎 handleEvent(StopAction+ViewTreeChanged+any)`() { - // When - fakeEvent = RumRawEvent.StopAction(fakeType, fakeName, emptyMap()) - val result = testedScope.handleEvent(fakeEvent, mockWriter) - val duration = measureNanoTime { - repeat(10) { - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS / 3) - testedScope.handleEvent(RumRawEvent.ViewTreeChanged(Time()), mockWriter) - } - } - testedScope.handleEvent(RumRawEvent.ViewTreeChanged(Time()), mockWriter) - Thread.sleep(RumActionScope.ACTION_INACTIVITY_MS) - val result2 = testedScope.handleEvent(mockEvent(), mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasActionData { - hasId(testedScope.actionId) - hasTimestamp(fakeEventTime.timestamp) - hasType(fakeType) - hasTargetName(fakeName) - hasDurationGreaterThan(duration) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasUserInfo(fakeUserInfo) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(result2).isNull() - } - - // region Internal - - private fun mockEvent(): RumRawEvent { - val event: RumRawEvent = mock() - whenever(event.eventTime) doReturn Time() - return event - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExtTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExtTest.kt deleted file mode 100644 index 62bf63b75c..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExtTest.kt +++ /dev/null @@ -1,404 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.internal.domain.model.ErrorEvent -import com.datadog.android.rum.internal.domain.model.ResourceEvent -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.RepeatedTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumEventExtTest { - - @Test - @RepeatedTest(12) - fun `𝕄 return method 𝕎 toMethod() {valid name}`( - @Forgery method: ResourceEvent.Method - ) { - // Given - val name = method.name - - // When - val result = name.toMethod() - - // Then - assertThat(result).isEqualTo(method) - } - - @Test - fun `𝕄 return GET 𝕎 toMethod() {invalid name}`( - @StringForgery(type = StringForgeryType.NUMERICAL) name: String - ) { - // When - val result = name.toMethod() - - // Then - assertThat(result).isEqualTo(ResourceEvent.Method.GET) - } - - @Test - @RepeatedTest(12) - fun `𝕄 return method 𝕎 toErrorMethod() {valid name}`( - @Forgery method: ErrorEvent.Method - ) { - // Given - val name = method.name - - // When - val result = name.toErrorMethod() - - // Then - assertThat(result).isEqualTo(method) - } - - @Test - fun `𝕄 return GET 𝕎 toErrorMethod() {invalid name}`( - @StringForgery(type = StringForgeryType.NUMERICAL) name: String - ) { - // When - val result = name.toErrorMethod() - - // Then - assertThat(result).isEqualTo(ErrorEvent.Method.GET) - } - - @Test - @RepeatedTest(22) - fun `𝕄 return resource type 𝕎 toSchemaType()`( - @Forgery kind: RumResourceKind - ) { - // When - val result = kind.toSchemaType() - - // Then - if (kind == RumResourceKind.UNKNOWN) { - assertThat(result).isEqualTo(ResourceEvent.Type1.OTHER) - } else { - assertThat(kind.name).isEqualTo(result.name) - } - } - - @Test - @RepeatedTest(12) - fun `𝕄 return error source 𝕎 toSchemaSource()`( - @Forgery kind: RumErrorSource - ) { - // When - val result = kind.toSchemaSource() - - // Then - assertThat(kind.name).isEqualTo(result.name) - } - - @Test - @RepeatedTest(10) - fun `𝕄 return action type 𝕎 toSchemaType()`( - @Forgery type: RumActionType - ) { - // When - val result = type.toSchemaType() - - // Then - assertThat(type.name).isEqualTo(result.name) - } - - @Test - fun `𝕄 return connectivity 𝕎 toResourceConnectivity() {not connected}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - - // When - val result = networkInfo.toResourceConnectivity() - - // Then - assertThat(result).isEqualTo( - ResourceEvent.Connectivity( - ResourceEvent.Status.NOT_CONNECTED, - emptyList(), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toResourceConnectivity() {Wifi}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_WIFI) - - // When - val result = networkInfo.toResourceConnectivity() - - // Then - assertThat(result).isEqualTo( - ResourceEvent.Connectivity( - ResourceEvent.Status.CONNECTED, - listOf(ResourceEvent.Interface.WIFI), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toResourceConnectivity() {Wimax}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_WIMAX) - - // When - val result = networkInfo.toResourceConnectivity() - - // Then - assertThat(result).isEqualTo( - ResourceEvent.Connectivity( - ResourceEvent.Status.CONNECTED, - listOf(ResourceEvent.Interface.WIMAX), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toResourceConnectivity() {Ethernet}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_ETHERNET) - - // When - val result = networkInfo.toResourceConnectivity() - - // Then - assertThat(result).isEqualTo( - ResourceEvent.Connectivity( - ResourceEvent.Status.CONNECTED, - listOf(ResourceEvent.Interface.ETHERNET), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toResourceConnectivity() {Bluetooth}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_BLUETOOTH) - - // When - val result = networkInfo.toResourceConnectivity() - - // Then - assertThat(result).isEqualTo( - ResourceEvent.Connectivity( - ResourceEvent.Status.CONNECTED, - listOf(ResourceEvent.Interface.BLUETOOTH), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toResourceConnectivity() {Cellular}`( - forge: Forge - ) { - // Given - val connectivity = forge.anElementFrom( - NetworkInfo.Connectivity.NETWORK_2G, - NetworkInfo.Connectivity.NETWORK_3G, - NetworkInfo.Connectivity.NETWORK_4G, - NetworkInfo.Connectivity.NETWORK_5G, - NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, - NetworkInfo.Connectivity.NETWORK_CELLULAR - ) - val technology = forge.anAlphabeticalString() - val networkInfo = NetworkInfo(connectivity, cellularTechnology = technology) - - // When - val result = networkInfo.toResourceConnectivity() - - // Then - assertThat(result).isEqualTo( - ResourceEvent.Connectivity( - ResourceEvent.Status.CONNECTED, - listOf(ResourceEvent.Interface.CELLULAR), - ResourceEvent.Cellular(technology) - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toResourceConnectivity() {Other}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_OTHER) - - // When - val result = networkInfo.toResourceConnectivity() - - // Then - assertThat(result).isEqualTo( - ResourceEvent.Connectivity( - ResourceEvent.Status.CONNECTED, - listOf(ResourceEvent.Interface.OTHER), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toErrorConnectivity() {not connected}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED) - - // When - val result = networkInfo.toErrorConnectivity() - - // Then - assertThat(result).isEqualTo( - ErrorEvent.Connectivity( - ErrorEvent.Status.NOT_CONNECTED, - emptyList(), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toErrorConnectivity() {Wifi}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_WIFI) - - // When - val result = networkInfo.toErrorConnectivity() - - // Then - assertThat(result).isEqualTo( - ErrorEvent.Connectivity( - ErrorEvent.Status.CONNECTED, - listOf(ErrorEvent.Interface.WIFI), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toErrorConnectivity() {Wimax}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_WIMAX) - - // When - val result = networkInfo.toErrorConnectivity() - - // Then - assertThat(result).isEqualTo( - ErrorEvent.Connectivity( - ErrorEvent.Status.CONNECTED, - listOf(ErrorEvent.Interface.WIMAX), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toErrorConnectivity() {Ethernet}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_ETHERNET) - - // When - val result = networkInfo.toErrorConnectivity() - - // Then - assertThat(result).isEqualTo( - ErrorEvent.Connectivity( - ErrorEvent.Status.CONNECTED, - listOf(ErrorEvent.Interface.ETHERNET), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toErrorConnectivity() {Bluetooth}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_BLUETOOTH) - - // When - val result = networkInfo.toErrorConnectivity() - - // Then - assertThat(result).isEqualTo( - ErrorEvent.Connectivity( - ErrorEvent.Status.CONNECTED, - listOf(ErrorEvent.Interface.BLUETOOTH), - null - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toErrorConnectivity() {Cellular}`( - forge: Forge - ) { - // Given - val connectivity = forge.anElementFrom( - NetworkInfo.Connectivity.NETWORK_2G, - NetworkInfo.Connectivity.NETWORK_3G, - NetworkInfo.Connectivity.NETWORK_4G, - NetworkInfo.Connectivity.NETWORK_5G, - NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, - NetworkInfo.Connectivity.NETWORK_CELLULAR - ) - val technology = forge.anAlphabeticalString() - val networkInfo = NetworkInfo(connectivity, cellularTechnology = technology) - - // When - val result = networkInfo.toErrorConnectivity() - - // Then - assertThat(result).isEqualTo( - ErrorEvent.Connectivity( - ErrorEvent.Status.CONNECTED, - listOf(ErrorEvent.Interface.CELLULAR), - ErrorEvent.Cellular(technology) - ) - ) - } - - @Test - fun `𝕄 return connectivity 𝕎 toErrorConnectivity() {Other}`() { - // Given - val networkInfo = NetworkInfo(NetworkInfo.Connectivity.NETWORK_OTHER) - - // When - val result = networkInfo.toErrorConnectivity() - - // Then - assertThat(result).isEqualTo( - ErrorEvent.Connectivity( - ErrorEvent.Status.CONNECTED, - listOf(ErrorEvent.Interface.OTHER), - null - ) - ) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt deleted file mode 100644 index 7729331be7..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt +++ /dev/null @@ -1,923 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.log.internal.user.NoOpMutableUserInfoProvider -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.assertj.RumEventAssert.Companion.assertThat -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.forge.exhaustiveAttributes -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.atMost -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.same -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.TimeUnit -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumResourceScopeTest { - - lateinit var testedScope: RumResourceScope - - @Mock - lateinit var mockParentScope: RumScope - - @Mock - lateinit var mockEvent: RumRawEvent - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockWriter: Writer - - @Mock - lateinit var mockDetector: FirstPartyHostDetector - - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") - lateinit var fakeUrl: String - lateinit var fakeKey: String - lateinit var fakeMethod: String - lateinit var fakeAttributes: Map - - @Forgery - lateinit var fakeParentContext: RumContext - - @Forgery - lateinit var fakeUserInfo: UserInfo - - @Forgery - lateinit var fakeNetworkInfo: NetworkInfo - - lateinit var fakeEventTime: Time - - @BeforeEach - fun `set up`(forge: Forge) { - fakeEventTime = Time() - - RumFeature::class.java.setStaticValue("userInfoProvider", mockUserInfoProvider) - RumFeature::class.java.setStaticValue("networkInfoProvider", mockNetworkInfoProvider) - - fakeAttributes = forge.exhaustiveAttributes() - fakeKey = forge.anAsciiString() - fakeMethod = forge.anElementFrom("PUT", "POST", "GET", "DELETE") - mockEvent = mockEvent() - - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo - whenever(mockParentScope.getRumContext()) doReturn fakeParentContext - doAnswer { false }.whenever(mockDetector).isFirstPartyUrl(any()) - - testedScope = RumResourceScope( - mockParentScope, - fakeUrl, - fakeMethod, - fakeKey, - fakeEventTime, - fakeAttributes, - mockDetector - ) - } - - @AfterEach - fun `tear down`() { - RumFeature::class.java.setStaticValue("userInfoProvider", NoOpMutableUserInfoProvider()) - GlobalRum.globalAttributes.clear() - } - - @Test - fun `𝕄 send Resource 𝕎 handleEvent(StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(null) - hasSpanId(null) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send first party Resource 𝕎 handleEvent(StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - forge: Forge - ) { - // Given - doAnswer { true }.whenever(mockDetector).isFirstPartyUrl(fakeUrl) - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(null) - hasSpanId(null) - hasFirstParty(true) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Resource with trace info 𝕎 handleEvent(StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @StringForgery(type = StringForgeryType.HEXADECIMAL) fakeSpanId: String, - @StringForgery(type = StringForgeryType.HEXADECIMAL) fakeTraceId: String, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes().toMutableMap() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - attributes[RumAttributes.TRACE_ID] = fakeTraceId - attributes[RumAttributes.SPAN_ID] = fakeSpanId - - // When - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(fakeTraceId) - hasSpanId(fakeSpanId) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Resource with initial context 𝕎 handleEvent(StopResource)`( - @Forgery context: RumContext, - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - whenever(mockParentScope.getRumContext()) doReturn context - - // When - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(null) - hasSpanId(null) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send event with user extra attributes 𝕎 handleEvent(StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long - ) { - // When - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, emptyMap()) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(null) - hasSpanId(null) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Resource with global attributes 𝕎 handleEvent(StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - forge: Forge - ) { - // Given - val attributes = forge.aMap { anHexadecimalString() to anAsciiString() } - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - GlobalRum.globalAttributes.putAll(attributes) - - // When - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, emptyMap()) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(null) - hasSpanId(null) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Resource with timing 𝕎 handleEvent(AddResourceTiming+StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @Forgery timing: ResourceTiming, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - mockEvent = RumRawEvent.AddResourceTiming(fakeKey, timing) - val resultTiming = testedScope.handleEvent(mockEvent, mockWriter) - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasTiming(timing) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(null) - hasSpanId(null) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(resultTiming).isEqualTo(testedScope) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Resource 𝕎 handleEvent(AddResourceTiming+StopResource) {unrelated timing}`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @Forgery timing: ResourceTiming, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - mockEvent = RumRawEvent.AddResourceTiming("not_the_$fakeKey", timing) - val resultTiming = testedScope.handleEvent(mockEvent, mockWriter) - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasId(testedScope.resourceId) - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasNoTiming() - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasTraceId(null) - hasSpanId(null) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(resultTiming).isEqualTo(testedScope) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Error 𝕎 handleEvent(StopResourceWithError)`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - forge: Forge - ) { - // Given - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - mockEvent = RumRawEvent.StopResourceWithError(fakeKey, null, message, source, throwable) - - // When - Thread.sleep(500) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasMessage(message) - hasSource(source) - hasStackTrace(throwable.loggableStackTrace()) - isCrash(false) - hasResource(fakeUrl, fakeMethod, 0L) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Error with initial context 𝕎 handleEvent(StopResourceWithError)`( - @Forgery context: RumContext, - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - forge: Forge - ) { - // Given - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - mockEvent = RumRawEvent.StopResourceWithError(fakeKey, null, message, source, throwable) - whenever(mockParentScope.getRumContext()) doReturn context - - // When - Thread.sleep(500) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasMessage(message) - hasSource(source) - hasStackTrace(throwable.loggableStackTrace()) - isCrash(false) - hasResource(fakeUrl, fakeMethod, 0L) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 send Error with global attributes 𝕎 handleEvent(StopResourceWithError)`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @LongForgery(200, 600) statusCode: Long, - @Forgery throwable: Throwable, - forge: Forge - ) { - // Given - val attributes = forge.aMap { anHexadecimalString() to anAsciiString() } - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - GlobalRum.globalAttributes.putAll(attributes) - mockEvent = RumRawEvent.StopResourceWithError( - fakeKey, - statusCode, - message, - source, - throwable - ) - - // When - Thread.sleep(500) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasMessage(message) - hasSource(source) - hasStackTrace(throwable.loggableStackTrace()) - isCrash(false) - hasResource(fakeUrl, fakeMethod, statusCode) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isEqualTo(null) - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StopResource) with different key`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - forge: Forge - ) { - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - mockEvent = RumRawEvent.StopResource("not_the_$fakeKey", statusCode, size, kind, attributes) - - Thread.sleep(500) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - verify(mockParentScope, atMost(1)).getRumContext() - verifyNoMoreInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StopResourceWithError) with different key`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @LongForgery(200, 600) statusCode: Long, - @Forgery throwable: Throwable, - forge: Forge - ) { - mockEvent = RumRawEvent.StopResourceWithError( - "not_the_$fakeKey", - statusCode, - message, - source, - throwable - ) - - Thread.sleep(500) - val result = testedScope.handleEvent(mockEvent, mockWriter) - - verify(mockParentScope, atMost(1)).getRumContext() - verifyNoMoreInteractions(mockWriter, mockParentScope) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(WaitForResourceTiming+StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - forge: Forge - ) { - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - mockEvent = RumRawEvent.WaitForResourceTiming(fakeKey) - val resultWaitForTiming = testedScope.handleEvent(mockEvent, mockWriter) - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val resultStop = testedScope.handleEvent(mockEvent, mockWriter) - - verify(mockParentScope, atMost(1)).getRumContext() - verifyNoMoreInteractions(mockWriter, mockParentScope) - assertThat(resultWaitForTiming).isEqualTo(testedScope) - assertThat(resultStop).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Resource 𝕎 handleEvent(WaitForResourceTiming+StopResource) {unrelated wait}`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - forge: Forge - ) { - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - mockEvent = RumRawEvent.WaitForResourceTiming("not_the_$fakeKey") - val resultWaitForTiming = testedScope.handleEvent(mockEvent, mockWriter) - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val resultStop = testedScope.handleEvent(mockEvent, mockWriter) - - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(resultWaitForTiming).isSameAs(testedScope) - assertThat(resultStop).isEqualTo(null) - } - - @Test - fun `𝕄 send Resource 𝕎 handleEvent(WaitForResourceTiming+AddResourceTiming+StopResource)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @Forgery timing: ResourceTiming, - forge: Forge - ) { - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - mockEvent = RumRawEvent.WaitForResourceTiming(fakeKey) - val resultWaitForTiming = testedScope.handleEvent(mockEvent, mockWriter) - mockEvent = RumRawEvent.AddResourceTiming(fakeKey, timing) - val resultTiming = testedScope.handleEvent(mockEvent, mockWriter) - Thread.sleep(500) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val resultStop = testedScope.handleEvent(mockEvent, mockWriter) - - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(resultWaitForTiming).isEqualTo(testedScope) - assertThat(resultTiming).isEqualTo(testedScope) - assertThat(resultStop).isEqualTo(null) - } - - @Test - fun `𝕄 send Resource 𝕎 handleEvent(WaitForResourceTiming+StopResource+AddResourceTiming)`( - @Forgery kind: RumResourceKind, - @LongForgery(200, 600) statusCode: Long, - @LongForgery(0, 1024) size: Long, - @Forgery timing: ResourceTiming, - forge: Forge - ) { - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - mockEvent = RumRawEvent.WaitForResourceTiming(fakeKey) - val resultWaitForTiming = testedScope.handleEvent(mockEvent, mockWriter) - mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) - val resultStop = testedScope.handleEvent(mockEvent, mockWriter) - Thread.sleep(500) - mockEvent = RumRawEvent.AddResourceTiming(fakeKey, timing) - val resultTiming = testedScope.handleEvent(mockEvent, mockWriter) - - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasResourceData { - hasTimestamp(fakeEventTime.timestamp) - hasUrl(fakeUrl) - hasMethod(fakeMethod) - hasKind(kind) - hasDurationGreaterThan(TimeUnit.MILLISECONDS.toNanos(500)) - hasTiming(timing) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(fakeParentContext.viewId, fakeParentContext.viewUrl) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeParentContext.actionId) - hasFirstParty(null) - } - } - verify(mockParentScope).handleEvent( - isA(), - same(mockWriter) - ) - verifyNoMoreInteractions(mockWriter) - assertThat(resultWaitForTiming).isEqualTo(testedScope) - assertThat(resultStop).isEqualTo(testedScope) - assertThat(resultTiming).isEqualTo(null) - } - - // region Internal - - private fun mockEvent(): RumRawEvent { - val event: RumRawEvent = mock() - whenever(event.eventTime) doReturn Time() - return event - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt deleted file mode 100644 index 4762210c7a..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt +++ /dev/null @@ -1,459 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import android.util.Log -import com.datadog.android.Datadog -import com.datadog.android.core.internal.data.NoOpWriter -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.forge.exhaustiveAttributes -import com.datadog.android.utils.mockDevLogHandler -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.same -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.FloatForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.UUID -import java.util.concurrent.TimeUnit -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.data.Offset -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumSessionScopeTest { - - lateinit var testedScope: RumSessionScope - - @Mock - lateinit var mockParentScope: RumScope - - @Mock - lateinit var mockChildScope: RumScope - - @Mock - lateinit var mockEvent: RumRawEvent - - @Mock - lateinit var mockWriter: Writer - - @Mock - lateinit var mockDetector: FirstPartyHostDetector - - lateinit var mockDevLogHandler: LogHandler - - @Forgery - lateinit var fakeParentContext: RumContext - - @Forgery - lateinit var fakeUserInfo: UserInfo - - @FloatForgery(min = 0f, max = 100f) - var fakeSamplingRate: Float = 0f - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @BeforeEach - fun `set up`() { - RumFeature::class.java.setStaticValue("userInfoProvider", mockUserInfoProvider) - - mockDevLogHandler = mockDevLogHandler() - - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - whenever(mockParentScope.getRumContext()) doReturn fakeParentContext - whenever(mockChildScope.handleEvent(any(), any())) doReturn mockChildScope - testedScope = RumSessionScope( - mockParentScope, - 100f, - mockDetector, - TEST_INACTIVITY_NS, - TEST_MAX_DURATION_NS - ) - - assertThat(GlobalRum.getRumContext()).isEqualTo(testedScope.getRumContext()) - } - - @Test - fun `updates sessionId if first call`() { - val context = testedScope.getRumContext() - - assertThat(context.sessionId) - .isNotEqualTo(RumContext.NULL_UUID) - .isEqualTo(testedScope.sessionId) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) - } - - @Test - fun `returns context with null IDs if session not kept`() { - testedScope = RumSessionScope( - mockParentScope, - 0f, - mockDetector, - TEST_INACTIVITY_NS, - TEST_MAX_DURATION_NS - ) - val context = testedScope.getRumContext() - - assertThat(context.sessionId).isEqualTo(RumContext.NULL_UUID) - assertThat(context.applicationId).isEqualTo(RumContext.NULL_UUID) - } - - @Test - fun `updates sessionId if last interaction too old`() { - val firstSessionId = testedScope.getRumContext().sessionId - - Thread.sleep(TEST_INACTIVITY_MS) - val context = testedScope.getRumContext() - - assertThat(context.sessionId) - .isNotEqualTo(UUID(0, 0)) - .isNotEqualTo(firstSessionId) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) - } - - @Test - fun `updates sessionId if duration is too long`() { - val firstSessionId = testedScope.getRumContext().sessionId - - Thread.sleep(TEST_MAX_DURATION_MS) - val context = testedScope.getRumContext() - - assertThat(context.sessionId) - .isNotEqualTo(UUID(0, 0)) - .isNotEqualTo(firstSessionId) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) - } - - @Test - fun `updates sessionId if duration is too long with updates`() { - val repeatCount = (TEST_MAX_DURATION_MS / TEST_SLEEP_MS) + 1 - val firstSessionId = testedScope.getRumContext().sessionId - - for (i in 0..repeatCount) { - Thread.sleep(TEST_SLEEP_MS) - testedScope.handleEvent(mockEvent, mockWriter) - } - Thread.sleep(TEST_SLEEP_MS) - val context = testedScope.getRumContext() - - assertThat(context.sessionId) - .isNotEqualTo(UUID(0, 0)) - .isNotEqualTo(firstSessionId) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) - } - - @Test - fun `updates sessionId on ResetSession event `() { - val firstSessionId = testedScope.getRumContext().sessionId - mockEvent = RumRawEvent.ResetSession() - - testedScope.handleEvent(mockEvent, mockWriter) - val context = testedScope.getRumContext() - - assertThat(context.sessionId) - .isNotEqualTo(UUID(0, 0)) - .isNotEqualTo(firstSessionId) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) - } - - @Test - fun `keeps sessionId if last interaction is recent`() { - val repeatCount = (TEST_INACTIVITY_MS / TEST_SLEEP_MS) + 1 - val firstSessionId = testedScope.getRumContext().sessionId - - for (i in 0..repeatCount) { - Thread.sleep(TEST_SLEEP_MS) - testedScope.handleEvent(mockEvent, mockWriter) - } - val context = testedScope.getRumContext() - - assertThat(context.sessionId).isEqualTo(firstSessionId) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) - } - - @Test - fun `updates keepSession state based on sampling rate`() { - testedScope = RumSessionScope( - mockParentScope, - fakeSamplingRate, - mockDetector, - TEST_INACTIVITY_NS, - TEST_MAX_DURATION_NS - ) - var sessions = 0 - var sessionsKept = 0 - - repeat(512) { - testedScope.handleEvent(RumRawEvent.ResetSession(), mockWriter) - val context = testedScope.getRumContext() - if (testedScope.keepSession) { - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.sessionId).isEqualTo(testedScope.sessionId) - } else { - assertThat(context.applicationId).isEqualTo(RumContext.NULL_UUID) - assertThat(context.sessionId).isEqualTo(RumContext.NULL_UUID) - } - sessions++ - if (testedScope.keepSession) sessionsKept++ - } - - val actualRate = (sessionsKept.toFloat() * 100f) / sessions - assertThat(actualRate).isCloseTo(fakeSamplingRate, Offset.offset(5f)) - } - - @Test - fun `M log warning W handleEvent() without child scope`() { - // Given - Datadog.setVerbosity(Log.VERBOSE) - - // When - val result = testedScope.handleEvent(mockEvent, mockWriter) - - // Then - assertThat(testedScope.activeChildrenScopes).isEmpty() - assertThat(result).isSameAs(testedScope) - verify(mockDevLogHandler).handleLog(Log.WARN, RumSessionScope.MESSAGE_MISSING_VIEW) - verifyNoMoreInteractions(mockDevLogHandler, mockWriter) - } - - @Test - fun `M delegate to child scope W handleEvent()`() { - testedScope.activeChildrenScopes.add(mockChildScope) - - val result = testedScope.handleEvent(mockEvent, mockWriter) - - verify(mockChildScope).handleEvent(mockEvent, mockWriter) - assertThat(testedScope.activeChildrenScopes).containsExactly(mockChildScope) - assertThat(result).isSameAs(testedScope) - verifyZeroInteractions(mockWriter, mockDevLogHandler) - } - - @Test - fun `M delegate to child scope with noop writer W handleEvent() and session not kept`() { - testedScope = RumSessionScope( - mockParentScope, - 0f, - mockDetector, - TEST_INACTIVITY_NS, - TEST_MAX_DURATION_NS - ) - testedScope.activeChildrenScopes.add(mockChildScope) - - val result = testedScope.handleEvent(mockEvent, mockWriter) - - argumentCaptor> { - verify(mockChildScope).handleEvent(same(mockEvent), capture()) - - assertThat(firstValue) - .isNotSameAs(mockWriter) - .isInstanceOf(NoOpWriter::class.java) - } - assertThat(testedScope.activeChildrenScopes).containsExactly(mockChildScope) - assertThat(result).isSameAs(testedScope) - verifyZeroInteractions(mockWriter, mockDevLogHandler) - } - - @Test - fun `M update child scope W handleEvent(StartView)`( - @StringForgery key: String, - @StringForgery name: String, - forge: Forge - ) { - val attributes = forge.exhaustiveAttributes() - mockEvent = RumRawEvent.StartView(key, name, attributes) - - val result = testedScope.handleEvent(mockEvent, mockWriter) - - assertThat(testedScope.activeChildrenScopes).hasSize(1) - val viewScope = testedScope.activeChildrenScopes.first() as RumViewScope - assertThat(viewScope.keyRef.get()).isEqualTo(key) - assertThat(viewScope.name).isEqualTo(name) - assertThat(viewScope.attributes).isEqualTo(attributes) - assertThat(viewScope.firstPartyHostDetector).isSameAs(mockDetector) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `M update child scope W handleEvent(StartView) event with existing children`( - @StringForgery key: String, - @StringForgery name: String, - forge: Forge - ) { - testedScope.activeChildrenScopes.add(mockChildScope) - val attributes = forge.exhaustiveAttributes() - mockEvent = RumRawEvent.StartView(key, name, attributes) - - val result = testedScope.handleEvent(mockEvent, mockWriter) - - verify(mockChildScope).handleEvent(mockEvent, mockWriter) - assertThat(testedScope.activeChildrenScopes).hasSize(2) - val viewScope = testedScope.activeChildrenScopes.last() as RumViewScope - assertThat(viewScope.keyRef.get()).isEqualTo(key) - assertThat(viewScope.name).isEqualTo(name) - assertThat(viewScope.attributes).containsAllEntriesOf(attributes) - assertThat(viewScope.firstPartyHostDetector).isSameAs(mockDetector) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `M keep children scope W handleEvent child returns non null`() { - testedScope.activeChildrenScopes.add(mockChildScope) - whenever(mockChildScope.handleEvent(mockEvent, mockWriter)) doReturn mockChildScope - - val result = testedScope.handleEvent(mockEvent, mockWriter) - - assertThat(testedScope.activeChildrenScopes).containsExactly(mockChildScope) - assertThat(result).isSameAs(testedScope) - verifyZeroInteractions(mockWriter) - } - - @Test - fun `M remove children scope W handleEvent child returns null`() { - testedScope.activeChildrenScopes.add(mockChildScope) - val newChildScope: RumScope = mock() - whenever(mockChildScope.handleEvent(mockEvent, mockWriter)) doReturn null - - val result = testedScope.handleEvent(mockEvent, mockWriter) - - assertThat(testedScope.activeChildrenScopes).isEmpty() - assertThat(result).isSameAs(testedScope) - verifyZeroInteractions(mockWriter) - } - - @Test - fun `M send ApplicationStarted event W applicationDisplayed`( - @StringForgery key: String, - @StringForgery name: String - ) { - val childView: RumViewScope = mock() - val startViewEvent = RumRawEvent.StartView(key, name, emptyMap()) - - testedScope.onApplicationDisplayed(startViewEvent, childView, mockWriter) - - argumentCaptor { - verify(childView).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.ApplicationStarted - assertThat(event.applicationStartupNanos).isEqualTo(Datadog.startupTimeNs) - } - verifyZeroInteractions(mockWriter) - } - - @Test - fun `M send ApplicationStarted event only once W applicationDisplayed`( - @StringForgery key: String, - @StringForgery name: String - ) { - val childView: RumViewScope = mock() - val startViewEvent = RumRawEvent.StartView(key, name, emptyMap()) - - testedScope.onApplicationDisplayed(startViewEvent, childView, mockWriter) - testedScope.onApplicationDisplayed(startViewEvent, childView, mockWriter) - - argumentCaptor { - verify(childView).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.ApplicationStarted - assertThat(event.applicationStartupNanos).isEqualTo(Datadog.startupTimeNs) - } - verifyZeroInteractions(mockWriter) - } - - @Test - fun `M send ApplicationStarted event W applicationDisplayed after ResetSession`( - @StringForgery key: String, - @StringForgery name: String - ) { - val childView: RumViewScope = mock() - val startViewEvent = RumRawEvent.StartView(key, name, emptyMap()) - - testedScope.onApplicationDisplayed(startViewEvent, childView, mockWriter) - val resetNanos = System.nanoTime() - testedScope.handleEvent(RumRawEvent.ResetSession(), mockWriter) - testedScope.onApplicationDisplayed(startViewEvent, childView, mockWriter) - - argumentCaptor { - verify(childView, times(2)).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.ApplicationStarted - assertThat(event.applicationStartupNanos).isEqualTo(Datadog.startupTimeNs) - val event2 = lastValue as RumRawEvent.ApplicationStarted - assertThat(event2.applicationStartupNanos).isGreaterThanOrEqualTo(resetNanos) - } - verifyZeroInteractions(mockWriter) - } - - @Test - fun `M do nothing W applicationDisplayed if session not kept`( - @StringForgery key: String, - @StringForgery name: String - ) { - testedScope = RumSessionScope( - mockParentScope, - 0f, - mockDetector, - TEST_INACTIVITY_NS, - TEST_MAX_DURATION_NS - ) - val startViewEvent = RumRawEvent.StartView(key, name, emptyMap()) - - val result = testedScope.handleEvent(startViewEvent, mockWriter) - - assertThat(testedScope.activeChildrenScopes).isNotEmpty() - assertThat(result).isSameAs(testedScope) - verifyZeroInteractions(mockWriter) - } - - companion object { - - private const val TEST_SLEEP_MS = 200L - private const val TEST_INACTIVITY_MS = TEST_SLEEP_MS * 3 - private const val TEST_MAX_DURATION_MS = TEST_SLEEP_MS * 10 - - private val TEST_INACTIVITY_NS = TimeUnit.MILLISECONDS.toNanos(TEST_INACTIVITY_MS) - private val TEST_MAX_DURATION_NS = TimeUnit.MILLISECONDS.toNanos(TEST_MAX_DURATION_MS) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt deleted file mode 100644 index a585b4cf2d..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt +++ /dev/null @@ -1,1974 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.scope - -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.core.internal.net.info.NetworkInfo -import com.datadog.android.core.internal.net.info.NetworkInfoProvider -import com.datadog.android.core.internal.utils.loggableStackTrace -import com.datadog.android.log.internal.user.NoOpMutableUserInfoProvider -import com.datadog.android.log.internal.user.UserInfo -import com.datadog.android.log.internal.user.UserInfoProvider -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.assertj.RumEventAssert.Companion.assertThat -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.model.ActionEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.forge.exhaustiveAttributes -import com.datadog.tools.unit.setStaticValue -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.BoolForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.UUID -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumViewScopeTest { - - lateinit var testedScope: RumViewScope - - @Mock - lateinit var mockParentScope: RumScope - - @Mock - lateinit var mockChildScope: RumScope - - @Mock - lateinit var mockActionScope: RumActionScope - - @Mock - lateinit var mockUserInfoProvider: UserInfoProvider - - @Mock - lateinit var mockNetworkInfoProvider: NetworkInfoProvider - - @Mock - lateinit var mockWriter: Writer - - @Mock - lateinit var mockDetector: FirstPartyHostDetector - - @RegexForgery("([a-z]+\\.)+[A-Z][a-z]+") - lateinit var fakeName: String - - @RegexForgery("[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}") - lateinit var fakeActionId: String - lateinit var fakeKey: ByteArray - lateinit var fakeAttributes: Map - - @Forgery - lateinit var fakeParentContext: RumContext - - @Forgery - lateinit var fakeUserInfo: UserInfo - - @Forgery - lateinit var fakeNetworkInfo: NetworkInfo - - lateinit var fakeEventTime: Time - - lateinit var fakeEvent: RumRawEvent - - @BeforeEach - fun `set up`(forge: Forge) { - fakeEventTime = Time() - - RumFeature::class.java.setStaticValue("userInfoProvider", mockUserInfoProvider) - RumFeature::class.java.setStaticValue("networkInfoProvider", mockNetworkInfoProvider) - - fakeAttributes = forge.exhaustiveAttributes() - fakeKey = forge.anAsciiString().toByteArray() - fakeEvent = mockEvent() - - whenever(mockUserInfoProvider.getUserInfo()) doReturn fakeUserInfo - whenever(mockNetworkInfoProvider.getLatestNetworkInfo()) doReturn fakeNetworkInfo - whenever(mockParentScope.getRumContext()) doReturn fakeParentContext - whenever(mockChildScope.handleEvent(any(), any())) doReturn mockChildScope - whenever(mockActionScope.handleEvent(any(), any())) doReturn mockActionScope - whenever(mockActionScope.actionId) doReturn fakeActionId - - testedScope = RumViewScope( - mockParentScope, - fakeKey, - fakeName, - fakeEventTime, - fakeAttributes, - mockDetector - ) - - assertThat(GlobalRum.getRumContext()).isEqualTo(testedScope.getRumContext()) - } - - @AfterEach - fun `tear down`() { - RumFeature::class.java.setStaticValue("userInfoProvider", NoOpMutableUserInfoProvider()) - GlobalRum.globalAttributes.clear() - } - - // region Context - - @Test - fun `𝕄 return valid RumContext 𝕎 getRumContext()`() { - // When - val context = testedScope.getRumContext() - - // Then - assertThat(context.actionId).isNull() - assertThat(context.viewId).isEqualTo(testedScope.viewId) - assertThat(context.viewUrl).isEqualTo(testedScope.urlName) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.sessionId).isEqualTo(fakeParentContext.sessionId) - } - - @Test - fun `𝕄 return active actionId 𝕎 getRumContext() with child ActionScope`() { - // Given - testedScope.activeActionScope = mockActionScope - - // When - val context = testedScope.getRumContext() - - // Then - assertThat(context.actionId).isEqualTo(fakeActionId) - assertThat(context.viewId).isEqualTo(testedScope.viewId) - assertThat(context.viewUrl).isEqualTo(testedScope.urlName) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - assertThat(context.sessionId).isEqualTo(fakeParentContext.sessionId) - } - - @Test - fun `𝕄 update the viewId 𝕎 getRumContext() with parent sessionId changed`( - @Forgery newSessionId: UUID - ) { - // Given - val initialViewId = testedScope.viewId - val context = testedScope.getRumContext() - whenever(mockParentScope.getRumContext()) - .doReturn(fakeParentContext.copy(sessionId = newSessionId.toString())) - - // When - val updatedContext = testedScope.getRumContext() - - // Then - assertThat(context.actionId).isNull() - assertThat(context.viewId).isEqualTo(initialViewId) - assertThat(context.viewUrl).isEqualTo(testedScope.urlName) - assertThat(context.sessionId).isEqualTo(fakeParentContext.sessionId) - assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) - - assertThat(updatedContext.actionId).isNull() - assertThat(updatedContext.viewId).isNotEqualTo(initialViewId) - assertThat(updatedContext.viewUrl).isEqualTo(testedScope.urlName) - assertThat(updatedContext.sessionId).isEqualTo(newSessionId.toString()) - assertThat(updatedContext.applicationId).isEqualTo(fakeParentContext.applicationId) - } - - // endregion - - // region View - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StartView) on stopped view`( - @StringForgery key: String, - @StringForgery name: String - ) { - // Given - testedScope.stopped = true - - // When - val result = testedScope.handleEvent( - RumRawEvent.StartView(key, name, emptyMap()), - mockWriter - ) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(StartView) on active view`( - @StringForgery key: String, - @StringForgery name: String - ) { - // When - val result = testedScope.handleEvent( - RumRawEvent.StartView(key, name, emptyMap()), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event once 𝕎 handleEvent(StartView) twice on active view`( - @StringForgery key: String, - @StringForgery name: String, - @StringForgery key2: String, - @StringForgery name2: String - ) { - // When - val result = testedScope.handleEvent( - RumRawEvent.StartView(key, name, emptyMap()), - mockWriter - ) - val result2 = testedScope.handleEvent( - RumRawEvent.StartView(key2, name2, emptyMap()), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - assertThat(result2).isNull() - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(StopView) on active view`( - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, attributes), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event with user extra attributes 𝕎 handleEvent(StopView) on active view`() { - // When - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, emptyMap()), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event with global attributes 𝕎 handleEvent(StopView) on active view`( - forge: Forge - ) { - // Given - val attributes = forge.aMap { anHexadecimalString() to anAsciiString() } - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - GlobalRum.globalAttributes.putAll(attributes) - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, emptyMap()), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 not take into account global attribute removal 𝕎 handleEvent(StopView) on active view`( - forge: Forge - ) { - // Given - GlobalRum.globalAttributes.clear() - val fakeGlobalAttributeKey = forge.anAlphabeticalString() - val fakeGlobalAttributeValue = forge.anAlphabeticalString() - GlobalRum.addAttribute(fakeGlobalAttributeKey, fakeGlobalAttributeValue) - testedScope = RumViewScope( - mockParentScope, - fakeKey, - fakeName, - fakeEventTime, - fakeAttributes, - mockDetector - ) - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.put(fakeGlobalAttributeKey, fakeGlobalAttributeValue) - - // When - GlobalRum.removeAttribute(fakeGlobalAttributeKey) - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, emptyMap()), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 take into account global attribute update 𝕎 handleEvent(StopView) on active view`( - forge: Forge - ) { - // Given - GlobalRum.globalAttributes.clear() - val fakeGlobalAttributeKey = forge.anAlphabeticalString() - val fakeGlobalAttributeValue = forge.anAlphabeticalString() - val fakeGlobalAttributeNewValue = - fakeGlobalAttributeValue + forge.anAlphabeticalString(size = 2) - GlobalRum.addAttribute(fakeGlobalAttributeKey, fakeGlobalAttributeValue) - testedScope = RumViewScope( - mockParentScope, - fakeKey, - fakeName, - fakeEventTime, - fakeAttributes, - mockDetector - ) - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.put(fakeGlobalAttributeKey, fakeGlobalAttributeNewValue) - - // When - GlobalRum.addAttribute(fakeGlobalAttributeKey, fakeGlobalAttributeNewValue) - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, emptyMap()), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event once 𝕎 handleEvent(StopView) twice on active view`( - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, attributes), - mockWriter - ) - val result2 = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, attributes), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - assertThat(result2).isNull() - } - - @Test - fun `𝕄 returns not null 𝕎 handleEvent(StopView) and a resource is still active`( - @StringForgery key: String, - forge: Forge - ) { - // Given - testedScope.activeResourceScopes.put(key, mockChildScope) - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, attributes), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(expectedAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(StopView) on active view with missing key`() { - // Given - fakeKey = ByteArray(0) - System.gc() - - // When - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, emptyMap()), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StopView) on active view without matching key`( - @StringForgery key: String, - @StringForgery name: String, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent( - RumRawEvent.StopView(key, attributes), - mockWriter - ) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StopView) on stopped view`( - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - val expectedAttributes = mutableMapOf() - expectedAttributes.putAll(fakeAttributes) - expectedAttributes.putAll(attributes) - testedScope.stopped = true - - // When - val result = testedScope.handleEvent( - RumRawEvent.StopView(fakeKey, attributes), - mockWriter - ) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(SentError) on active view`() { - // Given - fakeEvent = RumRawEvent.SentError() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(1) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(SentResource) on active view`() { - // Given - fakeEvent = RumRawEvent.SentResource() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(1) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(SentAction) on active view`() { - // Given - fakeEvent = RumRawEvent.SentAction() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(1) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event with global attributes 𝕎 handleEvent(ApplicationStarted) on active view`( - @StringForgery key: String, - @StringForgery name: String, - @LongForgery(0) duration: Long, - forge: Forge - ) { - // Given - val eventTime = Time() - val startedNanos = eventTime.nanoTime - duration - fakeEvent = RumRawEvent.ApplicationStarted(eventTime, startedNanos) - val attributes = forgeGlobalAttributes(forge, fakeAttributes) - GlobalRum.globalAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasActionData { - hasNonNullId() - hasTimestamp(testedScope.eventTimestamp) - hasType(ActionEvent.Type1.APPLICATION_START) - hasNoTarget() - hasDuration(duration) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasUserInfo(fakeUserInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - assertThat(lastValue) - .hasAttributes(fakeAttributes + attributes) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(1) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(SentError) on stopped view`() { - // Given - testedScope.stopped = true - fakeEvent = RumRawEvent.SentError() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(1) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(SentResource) on stopped view`() { - // Given - testedScope.stopped = true - fakeEvent = RumRawEvent.SentResource() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(1) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(SentAction) on stopped view`() { - // Given - testedScope.stopped = true - fakeEvent = RumRawEvent.SentAction() - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(1) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send events 𝕎 handleEvent(ApplicationStarted) on stopped view`( - @StringForgery key: String, - @StringForgery name: String, - @LongForgery(0) duration: Long - ) { - // Given - testedScope.stopped = true - val eventTime = Time() - val startedNanos = eventTime.nanoTime - duration - fakeEvent = RumRawEvent.ApplicationStarted(eventTime, startedNanos) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - assertThat(firstValue) - .hasActionData { - hasNonNullId() - hasTimestamp(testedScope.eventTimestamp) - hasType(ActionEvent.Type1.APPLICATION_START) - hasNoTarget() - hasDuration(duration) - hasResourceCount(0) - hasErrorCount(0) - hasCrashCount(0) - hasUserInfo(fakeUserInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(1) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(KeepAlive) on stopped view`( - @StringForgery key: String, - @StringForgery name: String - ) { - // Given - testedScope.stopped = true - - // When - val result = testedScope.handleEvent( - RumRawEvent.KeepAlive(), - mockWriter - ) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(KeepAlive) on active view`( - @StringForgery key: String, - @StringForgery name: String - ) { - // When - val result = testedScope.handleEvent( - RumRawEvent.KeepAlive(), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasVersion(2) - hasErrorCount(0) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - // endregion - - // region Action - - @Test - fun `𝕄 create ActionScope 𝕎 handleEvent(StartAction)`( - @Forgery type: RumActionType, - @StringForgery name: String, - @BoolForgery waitForStop: Boolean, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - - // When - val result = testedScope.handleEvent( - RumRawEvent.StartAction(type, name, waitForStop, attributes), - mockWriter - ) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(testedScope.activeActionScope).isInstanceOf(RumActionScope::class.java) - val actionScope = testedScope.activeActionScope as RumActionScope - assertThat(actionScope.name).isEqualTo(name) - assertThat(actionScope.waitForStop).isEqualTo(waitForStop) - assertThat(actionScope.attributes).containsAllEntriesOf(attributes) - assertThat(actionScope.parentScope).isSameAs(testedScope) - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StartAction) with active child ActionScope`( - @Forgery type: RumActionType, - @StringForgery name: String, - @BoolForgery waitForStop: Boolean, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - testedScope.activeActionScope = mockChildScope - fakeEvent = RumRawEvent.StartAction(type, name, waitForStop, attributes) - whenever(mockChildScope.handleEvent(fakeEvent, mockWriter)) doReturn mockChildScope - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockChildScope).handleEvent(fakeEvent, mockWriter) - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(testedScope.activeActionScope).isSameAs(mockChildScope) - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(StartAction) on stopped view`( - @Forgery type: RumActionType, - @StringForgery name: String, - @BoolForgery waitForStop: Boolean, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - testedScope.stopped = true - fakeEvent = RumRawEvent.StartAction(type, name, waitForStop, attributes) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isNull() - assertThat(testedScope.activeActionScope).isNull() - } - - @Test - fun `𝕄 send event to child ActionScope 𝕎 handleEvent(StartView) on active view`() { - // Given - testedScope.activeActionScope = mockChildScope - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockChildScope).handleEvent(fakeEvent, mockWriter) - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event to child ActionScope 𝕎 handleEvent() on stopped view`() { - // Given - testedScope.stopped = true - testedScope.activeActionScope = mockChildScope - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockChildScope).handleEvent(fakeEvent, mockWriter) - verifyZeroInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 remove child ActionScope 𝕎 handleEvent() returns null`() { - // Given - testedScope.activeActionScope = mockChildScope - whenever(mockChildScope.handleEvent(fakeEvent, mockWriter)) doReturn null - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockChildScope).handleEvent(fakeEvent, mockWriter) - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(testedScope.activeActionScope).isNull() - } - - // endregion - - // region Resource - - @Test - fun `𝕄 create ResourceScope 𝕎 handleEvent(StartResource)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - forge: Forge - ) { - // Given - val attributes = forge.exhaustiveAttributes() - - // When - val result = testedScope.handleEvent( - RumRawEvent.StartResource(key, url, method, attributes), - mockWriter - ) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(testedScope.activeResourceScopes).isNotEmpty() - val entry = testedScope.activeResourceScopes.entries.first() - assertThat(entry.key).isEqualTo(key) - assertThat(entry.value).isInstanceOf(RumResourceScope::class.java) - val resourceScope = entry.value as RumResourceScope - assertThat(resourceScope.parentScope).isSameAs(testedScope) - assertThat(resourceScope.attributes).containsAllEntriesOf(attributes) - assertThat(resourceScope.key).isSameAs(key) - assertThat(resourceScope.url).isEqualTo(url) - assertThat(resourceScope.method).isSameAs(method) - assertThat(resourceScope.firstPartyHostDetector).isSameAs(mockDetector) - } - - @Test - fun `𝕄 create ResourceScope with active actionId 𝕎 handleEvent(StartResource)`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.StartResource(key, url, method, attributes) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockActionScope).handleEvent(fakeEvent, mockWriter) - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(testedScope.activeResourceScopes).isNotEmpty() - val entry = testedScope.activeResourceScopes.entries.first() - assertThat(entry.key).isEqualTo(key) - val resourceScope = entry.value as RumResourceScope - assertThat(resourceScope.parentScope).isSameAs(testedScope) - assertThat(resourceScope.attributes).containsAllEntriesOf(attributes) - assertThat(resourceScope.key).isSameAs(key) - assertThat(resourceScope.url).isEqualTo(url) - assertThat(resourceScope.method).isSameAs(method) - assertThat(resourceScope.firstPartyHostDetector).isSameAs(mockDetector) - } - - @Test - fun `𝕄 send event to children ResourceScopes 𝕎 handleEvent(StartView) on active view`( - @StringForgery key: String - ) { - // Given - testedScope.activeResourceScopes[key] = mockChildScope - whenever(mockChildScope.handleEvent(fakeEvent, mockWriter)) doReturn mockChildScope - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockChildScope).handleEvent(fakeEvent, mockWriter) - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event to children ResourceScopes 𝕎 handleEvent(StartView) on stopped view`( - @StringForgery key: String - ) { - // Given - testedScope.stopped = true - testedScope.activeResourceScopes[key] = mockChildScope - whenever(mockChildScope.handleEvent(fakeEvent, mockWriter)) doReturn mockChildScope - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockChildScope).handleEvent(fakeEvent, mockWriter) - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 remove child ResourceScope 𝕎 handleEvent() returns null`( - @StringForgery key: String - ) { - // Given - testedScope.activeResourceScopes[key] = mockChildScope - whenever(mockChildScope.handleEvent(fakeEvent, mockWriter)) doReturn null - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verify(mockChildScope).handleEvent(fakeEvent, mockWriter) - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - assertThat(testedScope.activeResourceScopes).isEmpty() - } - - // endregion - - // region Error - - @Test - fun `𝕄 send events 𝕎 handleEvent(AddError) on active view`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - @StringForgery stacktrace: String, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - stacktrace, - false, - attributes - ) - - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasTimestamp(fakeEventTime.timestamp) - hasMessage(message) - hasSource(source) - hasStackTrace(stacktrace) - isCrash(false) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeActionId) - } - - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasErrorCount(1) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Error and View event 𝕎 AddError {throwable=null}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @StringForgery stacktrace: String, - forge: Forge - ) { - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.AddError( - message, - source, - null, - stacktrace, - false, - attributes - ) - - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasTimestamp(fakeEventTime.timestamp) - hasMessage(message) - hasSource(source) - hasStackTrace(stacktrace) - isCrash(false) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeActionId) - } - - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasErrorCount(1) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send Error and View event 𝕎 AddError {stacktrace=null}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - forge: Forge - ) { - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - false, - attributes - ) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasTimestamp(fakeEventTime.timestamp) - hasMessage(message) - hasSource(source) - hasStackTrace(throwable.loggableStackTrace()) - isCrash(false) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeActionId) - } - - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasErrorCount(1) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send events 𝕎 handleEvent(AddError) {throwable=null, stacktrace=null}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @BoolForgery fatal: Boolean, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.AddError(message, source, null, null, fatal, attributes) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasTimestamp(fakeEventTime.timestamp) - hasMessage(message) - hasSource(source) - hasStackTrace(null) - isCrash(fatal) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeActionId) - } - - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasErrorCount(1) - hasCrashCount(if (fatal) 1 else 0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send events with global attributes 𝕎 handleEvent(AddError)`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - false, - emptyMap() - ) - val attributes = forgeGlobalAttributes(forge, fakeAttributes) - GlobalRum.globalAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - val expectedViewAttributes = attributes.toMutableMap().apply { - putAll(fakeAttributes) - } - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasTimestamp(fakeEventTime.timestamp) - hasMessage(message) - hasSource(source) - hasStackTrace(throwable.loggableStackTrace()) - isCrash(false) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeActionId) - } - - assertThat(lastValue) - .hasAttributes(expectedViewAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasErrorCount(1) - hasCrashCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send events 𝕎 handleEvent(AddError) {isFatal=true}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - true, - attributes - ) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasTimestamp(fakeEventTime.timestamp) - hasMessage(message) - hasSource(source) - hasStackTrace(throwable.loggableStackTrace()) - isCrash(true) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeActionId) - } - - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasErrorCount(1) - hasCrashCount(1) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send events with global attributes 𝕎 handleEvent(AddError) {isFatal=true}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - fakeEvent = RumRawEvent.AddError( - message, - source, - throwable, - null, - true, - emptyMap() - ) - val attributes = forgeGlobalAttributes(forge, fakeAttributes) - GlobalRum.globalAttributes.putAll(attributes) - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - val expectedViewAttributes = attributes.toMutableMap().apply { - putAll(fakeAttributes) - } - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - - assertThat(firstValue) - .hasAttributes(attributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasErrorData { - hasTimestamp(fakeEventTime.timestamp) - hasMessage(message) - hasSource(source) - hasStackTrace(throwable.loggableStackTrace()) - isCrash(true) - hasUserInfo(fakeUserInfo) - hasConnectivityInfo(fakeNetworkInfo) - hasView(testedScope.viewId, testedScope.urlName) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - hasActionId(fakeActionId) - } - - assertThat(lastValue) - .hasAttributes(expectedViewAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasErrorCount(1) - hasCrashCount(1) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(AddError) on stopped view {throwable}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable, - @BoolForgery fatal: Boolean, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.AddError(message, source, throwable, null, fatal, attributes) - testedScope.stopped = true - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(AddError) on stopped view {stacktrace}`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @StringForgery stacktrace: String, - @BoolForgery fatal: Boolean, - forge: Forge - ) { - // Given - testedScope.activeActionScope = mockActionScope - val attributes = forge.exhaustiveAttributes() - fakeEvent = RumRawEvent.AddError(message, source, null, stacktrace, fatal, attributes) - testedScope.stopped = true - - // When - val result = testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isNull() - } - - // endregion - - // region Loading Time - - @Test - fun `𝕄 send event 𝕎 handleEvent(UpdateViewLoadingTime) on active view`(forge: Forge) { - // Given - val loadingTime = forge.aLong(min = 1) - val loadingType = forge.aValueFrom(ViewEvent.LoadingType::class.java) - - // When - val result = testedScope.handleEvent( - RumRawEvent.UpdateViewLoadingTime(fakeKey, loadingTime, loadingType), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasLoadingTime(loadingTime) - hasLoadingType(loadingType) - hasVersion(2) - hasErrorCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event 𝕎 handleEvent(UpdateViewLoadingTime) on stopped view`(forge: Forge) { - // Given - testedScope.stopped = true - val loadingTime = forge.aLong(min = 1) - val loadingType = forge.aValueFrom(ViewEvent.LoadingType::class.java) - - // When - val result = testedScope.handleEvent( - RumRawEvent.UpdateViewLoadingTime(fakeKey, loadingTime, loadingType), - mockWriter - ) - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasAttributes(fakeAttributes) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - hasName(fakeName.replace('.', '/')) - hasDurationGreaterThan(1) - hasLoadingTime(loadingTime) - hasLoadingType(loadingType) - hasVersion(2) - hasErrorCount(0) - hasResourceCount(0) - hasActionCount(0) - hasUserInfo(fakeUserInfo) - hasViewId(testedScope.viewId) - hasApplicationId(fakeParentContext.applicationId) - hasSessionId(fakeParentContext.sessionId) - } - } - verifyNoMoreInteractions(mockWriter) - assertThat(result).isNull() - } - - @Test - fun `𝕄 do nothing 𝕎 handleEvent(UpdateViewLoadingTime) with different key`(forge: Forge) { - // Given - val differentKey = fakeKey + "different".toByteArray() - val loadingTime = forge.aLong(min = 1) - val loadingType = forge.aValueFrom(ViewEvent.LoadingType::class.java) - - // When - val result = testedScope.handleEvent( - RumRawEvent.UpdateViewLoadingTime(differentKey, loadingTime, loadingType), - mockWriter - ) - - // Then - verifyZeroInteractions(mockWriter) - assertThat(result).isSameAs(testedScope) - } - - @Test - fun `𝕄 send event with custom timing 𝕎 handleEvent(AddCustomTiming) on active view`( - forge: Forge - ) { - // Given - val fakeTimingKey = forge.anAlphabeticalString() - - // When - testedScope.handleEvent( - RumRawEvent.AddCustomTiming(fakeTimingKey), - mockWriter - ) - val customTimingEstimatedDuration = System.nanoTime() - fakeEventTime.nanoTime - - // Then - argumentCaptor { - verify(mockWriter).write(capture()) - assertThat(lastValue) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasCustomTimings(mapOf(fakeTimingKey to customTimingEstimatedDuration)) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - } - } - verifyNoMoreInteractions(mockWriter) - } - - @Test - fun `𝕄 send event with custom timings 𝕎 handleEvent(AddCustomTiming) called multipe times`( - forge: Forge - ) { - // Given - val fakeTimingKey1 = forge.anAlphabeticalString() - val fakeTimingKey2 = forge.anAlphabeticalString() - - // When - testedScope.handleEvent( - RumRawEvent.AddCustomTiming(fakeTimingKey1), - mockWriter - ) - val customTiming1EstimatedDuration = System.nanoTime() - fakeEventTime.nanoTime - testedScope.handleEvent( - RumRawEvent.AddCustomTiming(fakeTimingKey2), - mockWriter - ) - val customTiming2EstimatedDuration = System.nanoTime() - fakeEventTime.nanoTime - - // Then - argumentCaptor { - verify(mockWriter, times(2)).write(capture()) - assertThat(firstValue) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasCustomTimings(mapOf(fakeTimingKey1 to customTiming1EstimatedDuration)) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - } - assertThat(lastValue) - .hasUserExtraAttributes(fakeUserInfo.extraInfo) - .hasCustomTimings( - mapOf( - fakeTimingKey1 to customTiming1EstimatedDuration, - fakeTimingKey2 to customTiming2EstimatedDuration - ) - ) - .hasViewData { - hasTimestamp(fakeEventTime.timestamp) - } - } - verifyNoMoreInteractions(mockWriter) - } - - // endregion - - // region Internal - - private fun mockEvent(): RumRawEvent { - val event: RumRawEvent = mock() - whenever(event.eventTime) doReturn Time() - return event - } - - private fun forgeGlobalAttributes( - forge: Forge, - existingAttributes: Map - ): Map { - val existingKeys = existingAttributes.keys - return forge.aMap { anHexadecimalString() to anAsciiString() } - .filter { it.key !in existingKeys } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyApi29Test.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyApi29Test.kt deleted file mode 100644 index 98189b1d70..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyApi29Test.kt +++ /dev/null @@ -1,50 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation - -import com.datadog.android.rum.ActivityLifecycleTrackingStrategyTest -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class GesturesTrackingStrategyApi29Test : ActivityLifecycleTrackingStrategyTest() { - - @Mock - lateinit var mockGesturesTracker: GesturesTracker - - @BeforeEach - override fun `set up`(forge: Forge) { - super.`set up`(forge) - testedStrategy = GesturesTrackingStrategyApi29(mockGesturesTracker) - } - - @Test - fun `when activity pre created it will start tracking gestures`(forge: Forge) { - // When - testedStrategy.onActivityPreCreated(mockActivity, mock()) - // Then - verify(mockGesturesTracker).startTracking(mockWindow, mockActivity) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyTest.kt deleted file mode 100644 index 216c52783c..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/GesturesTrackingStrategyTest.kt +++ /dev/null @@ -1,57 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation - -import com.datadog.android.rum.ActivityLifecycleTrackingStrategyTest -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.verify -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class GesturesTrackingStrategyTest : ActivityLifecycleTrackingStrategyTest() { - - @Mock - lateinit var mockGesturesTracker: GesturesTracker - - @BeforeEach - override fun `set up`(forge: Forge) { - super.`set up`(forge) - testedStrategy = GesturesTrackingStrategy(mockGesturesTracker) - } - - @Test - fun `when activity resumed it will start tracking gestures`(forge: Forge) { - // When - testedStrategy.onActivityResumed(mockActivity) - // Then - verify(mockGesturesTracker).startTracking(mockWindow, mockActivity) - } - - @Test - fun `when activity paused it will stop tracking gestures`(forge: Forge) { - // When - testedStrategy.onActivityPaused(mockActivity) - // Then - verify(mockGesturesTracker).stopTracking(mockWindow, mockActivity) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTrackerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTrackerTest.kt deleted file mode 100644 index 6bee19bc8e..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTrackerTest.kt +++ /dev/null @@ -1,119 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.app.Activity -import android.view.Window -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.spy -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class DatadogGesturesTrackerTest { - - lateinit var testedTracker: DatadogGesturesTracker - - @Mock - lateinit var mockActivity: Activity - - @Mock - lateinit var mockWindow: Window - - @Mock - lateinit var mockGestureDetector: GesturesDetectorWrapper - - @BeforeEach - fun `set up`() { - testedTracker = - DatadogGesturesTracker(emptyArray()) - whenever(mockActivity.window).thenReturn(mockWindow) - } - - @Test - fun `will start tracking the activity`() { - // When - val spyTest = spy(testedTracker) - doReturn(mockGestureDetector) - .whenever(spyTest) - .generateGestureDetector(mockActivity, mockWindow) - spyTest.startTracking(mockWindow, mockActivity) - - // Then - verify(mockWindow).callback = isA() - } - - @Test - fun `will stop tracking the activity`() { - // Given - whenever(mockWindow.callback) - .thenReturn( - WindowCallbackWrapper( - NoOpWindowCallback(), - mockGestureDetector - ) - ) - - // When - testedTracker.stopTracking(mockWindow, mockActivity) - - // Then - verify(mockWindow).callback = null - } - - @Test - fun `stop tracking the activity will restore the previous callback if was not null`() { - // Given - val previousCallback: Window.Callback = mock() - whenever(mockWindow.callback) - .thenReturn( - WindowCallbackWrapper( - previousCallback, - mockGestureDetector - ) - ) - - // When - testedTracker.stopTracking(mockWindow, mockActivity) - - // Then - verify(mockWindow).callback = previousCallback - } - - @Test - fun `stop will do nothing if the activity was not tracked`() { - // When - testedTracker.stopTracking(mockWindow, mockActivity) - - // Then - verify(mockWindow, never()).callback = any() - } - - @Test - fun `will not track an activity with no decor view`() { - // Given - whenever(mockWindow.decorView).thenReturn(null) - - // Then - verify(mockWindow, never()).callback = any() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesDetectorWrapperTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesDetectorWrapperTest.kt deleted file mode 100644 index d9ab78ba0b..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesDetectorWrapperTest.kt +++ /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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.view.MotionEvent -import androidx.core.view.GestureDetectorCompat -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class GesturesDetectorWrapperTest { - - lateinit var testedWrapper: GesturesDetectorWrapper - - @Mock - lateinit var mockGesturesDetectorListener: GesturesListener - - @Mock - lateinit var mockGesturesDetectorCompat: GestureDetectorCompat - - @BeforeEach - fun `set up`() { - testedWrapper = GesturesDetectorWrapper( - mockGesturesDetectorListener, - mockGesturesDetectorCompat - ) - } - - @Test - fun `it will delegate the events to the bundled compat detector`() { - val event: MotionEvent = mock() - testedWrapper.onTouchEvent(event) - verify(mockGesturesDetectorCompat).onTouchEvent(event) - } - - @Test - fun `on action up will call the gesture listener after delegating to gestures detector`() { - val event: MotionEvent = mock { - whenever(it.actionMasked).thenReturn(MotionEvent.ACTION_UP) - } - testedWrapper.onTouchEvent(event) - inOrder(mockGesturesDetectorCompat, mockGesturesDetectorListener) { - verify(mockGesturesDetectorCompat).onTouchEvent(event) - verify(mockGesturesDetectorListener).onUp(event) - } - } - - @Test - fun `in different than action up will not interact with the listener`(forge: Forge) { - val event: MotionEvent = mock { - whenever(it.actionMasked).thenReturn( - forge.anElementFrom( - MotionEvent.ACTION_DOWN, - MotionEvent.ACTION_SCROLL, - MotionEvent.ACTION_CANCEL, - MotionEvent.ACTION_MOVE - ) - ) - } - - verifyZeroInteractions(mockGesturesDetectorListener) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt deleted file mode 100644 index 49d1964c0c..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt +++ /dev/null @@ -1,630 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.AbsListView -import android.widget.ListAdapter -import androidx.core.view.ScrollingView -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.internal.monitor.DatadogRumMonitor -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.lang.ref.WeakReference -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() { - - @Mock - lateinit var mockDatadogRumMonitor: DatadogRumMonitor - - // region Tests - - @BeforeEach - override fun `set up`() { - super.`set up`() - GlobalRum.registerIfAbsent(mockDatadogRumMonitor) - } - - @Test - fun `it will send an scroll rum event if fling not detected`(forge: Forge) { - val startDownEvent: MotionEvent = forge.getForgery() - val listSize = forge.anInt(1, 20) - val intermediaryEvents = - forge.aList(size = listSize) { forge.getForgery(MotionEvent::class.java) } - val distancesX = forge.aList(listSize) { forge.aFloat() } - val distancesY = forge.aList(listSize) { forge.aFloat() } - val targetId = forge.anInt() - val endUpEvent = intermediaryEvents[intermediaryEvents.size - 1] - val expectedDirection = forge.anElementFrom( - GesturesListener.SCROLL_DIRECTION_DOWN, - GesturesListener.SCROLL_DIRECTION_UP, - GesturesListener.SCROLL_DIRECTION_LEFT, - GesturesListener.SCROLL_DIRECTION_RIGHT - ) - stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) - - val scrollingTarget: ScrollableView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = endUpEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(scrollingTarget, expectedResourceName) - val expectedAttributes: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, - RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection - ) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onDown(startDownEvent) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onUp(endUpEvent) - - // Then - - inOrder(mockDatadogRumMonitor) { - verify(mockDatadogRumMonitor).startUserAction(RumActionType.CUSTOM, "", emptyMap()) - val argumentCaptor = argumentCaptor>() - verify(mockDatadogRumMonitor).stopUserAction( - eq(RumActionType.SCROLL), - eq(targetName(scrollingTarget, expectedResourceName)), - argumentCaptor.capture() - ) - assertThat(argumentCaptor.firstValue).isEqualTo(expectedAttributes) - } - verifyNoMoreInteractions(mockDatadogRumMonitor) - } - - @Test - fun `it will send a scroll rum event if target is a ListView`(forge: Forge) { - val startDownEvent: MotionEvent = forge.getForgery() - val listSize = forge.anInt(1, 20) - val intermediaryEvents = - forge.aList(size = listSize) { forge.getForgery(MotionEvent::class.java) } - val distancesX = forge.aList(listSize) { forge.aFloat() } - val distancesY = forge.aList(listSize) { forge.aFloat() } - val targetId = forge.anInt() - val endUpEvent = intermediaryEvents[intermediaryEvents.size - 1] - val expectedDirection = forge.anElementFrom( - GesturesListener.SCROLL_DIRECTION_DOWN, - GesturesListener.SCROLL_DIRECTION_UP, - GesturesListener.SCROLL_DIRECTION_LEFT, - GesturesListener.SCROLL_DIRECTION_RIGHT - ) - stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) - - val scrollingTarget: ScrollableListView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(scrollingTarget, expectedResourceName) - val expectedAttributes: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, - RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection - ) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onDown(startDownEvent) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onUp(endUpEvent) - - // Then - - inOrder(mockDatadogRumMonitor) { - verify(mockDatadogRumMonitor).startUserAction(RumActionType.CUSTOM, "", emptyMap()) - val argumentCaptor = argumentCaptor>() - verify(mockDatadogRumMonitor).stopUserAction( - eq(RumActionType.SCROLL), - eq(targetName(scrollingTarget, expectedResourceName)), - argumentCaptor.capture() - ) - assertThat(argumentCaptor.firstValue).isEqualTo(expectedAttributes) - } - verifyNoMoreInteractions(mockDatadogRumMonitor) - } - - @Test - fun `it will reset the scroll data between 2 consecutive gestures`(forge: Forge) { - val startDownEvent: MotionEvent = forge.getForgery() - val listSize = forge.anInt(1, 20) - val intermediaryEvents = - forge.aList(size = listSize) { forge.getForgery(MotionEvent::class.java) } - val distancesX = forge.aList(listSize) { forge.aFloat() } - val distancesY = forge.aList(listSize) { forge.aFloat() } - val targetId = forge.anInt() - val endUpEvent = intermediaryEvents[intermediaryEvents.size - 1] - val expectedDirection1 = forge.anElementFrom( - GesturesListener.SCROLL_DIRECTION_DOWN, - GesturesListener.SCROLL_DIRECTION_UP, - GesturesListener.SCROLL_DIRECTION_LEFT, - GesturesListener.SCROLL_DIRECTION_RIGHT - ) - val expectedDirection2 = forge.anElementFrom( - GesturesListener.SCROLL_DIRECTION_DOWN, - GesturesListener.SCROLL_DIRECTION_UP, - GesturesListener.SCROLL_DIRECTION_LEFT, - GesturesListener.SCROLL_DIRECTION_RIGHT - ) - val scrollingTarget: ScrollableListView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(scrollingTarget, expectedResourceName) - val expectedAttributes1: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, - RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection1 - ) - val expectedAttributes2: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, - RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection2 - ) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onDown(startDownEvent) - stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection1) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onUp(endUpEvent) - - testedListener.onDown(startDownEvent) - stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection2) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onUp(endUpEvent) - - // Then - - inOrder(mockDatadogRumMonitor) { - verify(mockDatadogRumMonitor).startUserAction(RumActionType.CUSTOM, "", emptyMap()) - val argumentCaptor1 = argumentCaptor>() - verify(mockDatadogRumMonitor).stopUserAction( - eq(RumActionType.SCROLL), - eq(targetName(scrollingTarget, expectedResourceName)), - argumentCaptor1.capture() - ) - assertThat(argumentCaptor1.firstValue).isEqualTo(expectedAttributes1) - verify(mockDatadogRumMonitor).startUserAction(RumActionType.CUSTOM, "", emptyMap()) - val argumentCaptor2 = argumentCaptor>() - verify(mockDatadogRumMonitor).stopUserAction( - eq(RumActionType.SCROLL), - eq(targetName(scrollingTarget, expectedResourceName)), - argumentCaptor2.capture() - ) - assertThat(argumentCaptor2.firstValue).isEqualTo(expectedAttributes2) - } - verifyNoMoreInteractions(mockDatadogRumMonitor) - } - - @Test - fun `will do nothing if there was no valid target `(forge: Forge) { - val startDownEvent: MotionEvent = forge.getForgery() - val listSize = forge.anInt(1, 20) - val intermediaryEvents = - forge.aList(size = listSize) { forge.getForgery(MotionEvent::class.java) } - val distancesX = forge.aList(listSize) { forge.aFloat() } - val distancesY = forge.aList(listSize) { forge.aFloat() } - val targetId = forge.anInt() - val endUpEvent = intermediaryEvents[intermediaryEvents.size - 1] - val scrollingTarget: ScrollableListView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = false, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(scrollingTarget, expectedResourceName) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onDown(startDownEvent) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onUp(endUpEvent) - - // Then - verifyZeroInteractions(mockDatadogRumMonitor) - } - - @Test - fun `will do nothing if the registered monitor is not a DatadogRumMonitor`(forge: Forge) { - `tear down`() - val startDownEvent: MotionEvent = forge.getForgery() - val listSize = forge.anInt(1, 20) - val intermediaryEvents = - forge.aList(size = listSize) { forge.getForgery(MotionEvent::class.java) } - val distancesX = forge.aList(listSize) { forge.aFloat() } - val distancesY = forge.aList(listSize) { forge.aFloat() } - val targetId = forge.anInt() - val endUpEvent = intermediaryEvents[intermediaryEvents.size - 1] - val scrollingTarget: ScrollableListView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(scrollingTarget, expectedResourceName) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onDown(startDownEvent) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onUp(endUpEvent) - - // Then - verifyZeroInteractions(mockDatadogRumMonitor) - } - - @Test - fun `it will send a swipe rum event if detected`(forge: Forge) { - val listSize = forge.anInt(1, 20) - val startDownEvent: MotionEvent = forge.getForgery() - val intermediaryEvents = - forge.aList(size = listSize) { forge.getForgery(MotionEvent::class.java) } - val endUpEvent = intermediaryEvents[intermediaryEvents.size - 1] - val expectedDirection = forge.anElementFrom( - GesturesListener.SCROLL_DIRECTION_DOWN, - GesturesListener.SCROLL_DIRECTION_UP, - GesturesListener.SCROLL_DIRECTION_LEFT, - GesturesListener.SCROLL_DIRECTION_RIGHT - ) - stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) - val distancesX = forge.aList(listSize) { forge.aFloat() } - val distancesY = forge.aList(listSize) { forge.aFloat() } - val velocityX = forge.aFloat() - val velocityY = forge.aFloat() - val targetId = forge.anInt() - val scrollingTarget: ScrollableView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(scrollingTarget, expectedResourceName) - val expectedAttributes: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, - RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection - ) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onDown(startDownEvent) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onFling(startDownEvent, endUpEvent, velocityX, velocityY) - testedListener.onUp(endUpEvent) - - // Then - - inOrder(mockDatadogRumMonitor) { - verify(mockDatadogRumMonitor).startUserAction(RumActionType.CUSTOM, "", emptyMap()) - val argumentCaptor = argumentCaptor>() - verify(mockDatadogRumMonitor).stopUserAction( - eq(RumActionType.SWIPE), - eq(targetName(scrollingTarget, expectedResourceName)), - argumentCaptor.capture() - ) - assertThat(argumentCaptor.firstValue).isEqualTo(expectedAttributes) - } - verifyNoMoreInteractions(mockDatadogRumMonitor) - } - - @Test - fun `it will reset the swipe data between 2 consecutive gestures`(forge: Forge) { - val listSize = forge.anInt(1, 20) - val startDownEvent: MotionEvent = forge.getForgery() - val intermediaryEvents = - forge.aList(size = listSize) { forge.getForgery(MotionEvent::class.java) } - val endUpEvent = intermediaryEvents[intermediaryEvents.size - 1] - val expectedDirection1 = forge.anElementFrom( - GesturesListener.SCROLL_DIRECTION_DOWN, - GesturesListener.SCROLL_DIRECTION_UP, - GesturesListener.SCROLL_DIRECTION_LEFT, - GesturesListener.SCROLL_DIRECTION_RIGHT - ) - val expectedDirection2 = forge.anElementFrom( - GesturesListener.SCROLL_DIRECTION_DOWN, - GesturesListener.SCROLL_DIRECTION_UP, - GesturesListener.SCROLL_DIRECTION_LEFT, - GesturesListener.SCROLL_DIRECTION_RIGHT - ) - val distancesX = forge.aList(listSize) { forge.aFloat() } - val distancesY = forge.aList(listSize) { forge.aFloat() } - val velocityX = forge.aFloat() - val velocityY = forge.aFloat() - val targetId = forge.anInt() - val scrollingTarget: ScrollableView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(scrollingTarget, expectedResourceName) - val expectedAttributes1: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, - RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection1 - ) - val expectedAttributes2: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, - RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection2 - ) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection1) - testedListener.onDown(startDownEvent) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onFling(startDownEvent, endUpEvent, velocityX, velocityY) - testedListener.onUp(endUpEvent) - - stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection2) - testedListener.onDown(startDownEvent) - intermediaryEvents.forEachIndexed { index, event -> - testedListener.onScroll(startDownEvent, event, distancesX[index], distancesY[index]) - } - testedListener.onFling(startDownEvent, endUpEvent, velocityX, velocityY) - testedListener.onUp(endUpEvent) - - // Then - - inOrder(mockDatadogRumMonitor) { - verify(mockDatadogRumMonitor).startUserAction(RumActionType.CUSTOM, "", emptyMap()) - val argumentCaptor1 = argumentCaptor>() - verify(mockDatadogRumMonitor).stopUserAction( - eq(RumActionType.SWIPE), - eq(targetName(scrollingTarget, expectedResourceName)), - argumentCaptor1.capture() - ) - assertThat(argumentCaptor1.firstValue).isEqualTo(expectedAttributes1) - verify(mockDatadogRumMonitor).startUserAction(RumActionType.CUSTOM, "", emptyMap()) - val argumentCaptor2 = argumentCaptor>() - verify(mockDatadogRumMonitor).stopUserAction( - eq(RumActionType.SWIPE), - eq(targetName(scrollingTarget, expectedResourceName)), - argumentCaptor2.capture() - ) - assertThat(argumentCaptor2.firstValue).isEqualTo(expectedAttributes2) - } - verifyNoMoreInteractions(mockDatadogRumMonitor) - } - - @Test - fun `on touchUp will do nothing if there was no scroll or swipe event detected`(forge: Forge) { - val startDownEvent: MotionEvent = forge.getForgery() - val endUpEvent: MotionEvent = forge.getForgery() - val targetId = forge.anInt() - val scrollingTarget: ScrollableView = mockView( - id = targetId, - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = startDownEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(scrollingTarget) - } - - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - testedListener.onUp(startDownEvent) - testedListener.onDown(endUpEvent) - - verifyZeroInteractions(mockDatadogRumMonitor) - } - - // endregion - - // region internal - - internal class ScrollableView : View(mock()), ScrollingView { - override fun computeVerticalScrollOffset(): Int { - return 0 - } - - override fun computeVerticalScrollExtent(): Int { - return 0 - } - - override fun computeVerticalScrollRange(): Int { - return 0 - } - - override fun computeHorizontalScrollOffset(): Int { - return 0 - } - - override fun computeHorizontalScrollRange(): Int { - return 0 - } - - override fun computeHorizontalScrollExtent(): Int { - return 0 - } - } - - internal class ScrollableListView : AbsListView(mock()) { - override fun getAdapter(): ListAdapter { - return mock() - } - - override fun setSelection(position: Int) { - } - } - - private fun stubStopMotionEvent( - stopEvent: MotionEvent, - startEvent: MotionEvent, - direction: String - ) { - val initialStartX = startEvent.x - val initialStartY = startEvent.y - when (direction) { - GesturesListener.SCROLL_DIRECTION_UP -> { - whenever(stopEvent.x).thenReturn(initialStartX) - whenever(stopEvent.y).thenReturn((initialStartY - 2)) - } - GesturesListener.SCROLL_DIRECTION_DOWN -> { - whenever(stopEvent.x).thenReturn(initialStartX) - whenever(stopEvent.y).thenReturn((initialStartY + 2)) - } - GesturesListener.SCROLL_DIRECTION_LEFT -> { - whenever(stopEvent.x).thenReturn((initialStartX + 2)) - whenever(stopEvent.y).thenReturn(initialStartY) - } - GesturesListener.SCROLL_DIRECTION_RIGHT -> { - whenever(stopEvent.x).thenReturn((initialStartX - 2)) - whenever(stopEvent.y).thenReturn(initialStartY) - } - } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt deleted file mode 100644 index 7b1d593455..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt +++ /dev/null @@ -1,458 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.content.res.Resources -import android.util.Log -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.view.Window -import com.datadog.android.log.internal.logger.LogHandler -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.tracking.ViewAttributesProvider -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockDevLogHandler -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.lang.ref.WeakReference -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { - - @Mock - lateinit var mockRumMonitor: RumMonitor - - lateinit var mockDevLogHandler: LogHandler - - @BeforeEach - override fun `set up`() { - super.`set up`() - mockDevLogHandler = mockDevLogHandler() - GlobalRum.registerIfAbsent(mockRumMonitor) - } - - @Test - fun `onTap sends the right target when the ViewGroup and its child are both clickable`( - forge: Forge - ) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - val container1: ViewGroup = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = false, - forge = forge - ) - val target: View = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) - val notClickableInvalidTarget: View = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge - ) - val notVisibleInvalidTarget: View = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - visible = false, - forge = forge - ) - val container2: ViewGroup = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) { - whenever(it.childCount).thenReturn(3) - whenever(it.getChildAt(0)).thenReturn(notClickableInvalidTarget) - whenever(it.getChildAt(1)).thenReturn(notVisibleInvalidTarget) - whenever(it.getChildAt(2)).thenReturn(target) - } - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(2) - whenever(it.getChildAt(0)).thenReturn(container1) - whenever(it.getChildAt(1)).thenReturn(container2) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(target, expectedResourceName) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyMonitorCalledWithUserAction(target, expectedResourceName) - } - - @Test - fun `onTap dispatches an UserAction if target is ViewGroup and clickable`(forge: Forge) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - val target: ViewGroup = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) { - whenever(it.childCount).thenReturn(2) - whenever(it.getChildAt(0)).thenReturn(mock()) - whenever(it.getChildAt(1)).thenReturn(mock()) - } - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(target) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(target, expectedResourceName) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyMonitorCalledWithUserAction(target, expectedResourceName) - } - - @Test - fun `onTap ignores invisible or gone views`(forge: Forge) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - val invalidTarget: View = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - visible = false, - clickable = true, - forge = forge - ) - val validTarget: View = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(2) - whenever(it.getChildAt(0)).thenReturn(invalidTarget) - whenever(it.getChildAt(1)).thenReturn(validTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(validTarget, expectedResourceName) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyMonitorCalledWithUserAction(validTarget, expectedResourceName) - } - - @Test - fun `onTap ignores not clickable targets`(forge: Forge) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - val invalidTarget: View = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - clickable = false, - forge = forge - ) - val validTarget: View = mockView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(2) - whenever(it.getChildAt(0)).thenReturn(invalidTarget) - whenever(it.getChildAt(1)).thenReturn(validTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(validTarget, expectedResourceName) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyMonitorCalledWithUserAction(validTarget, expectedResourceName) - } - - @Test - fun `onTap does nothing if no children present and decor view not clickable`( - forge: Forge - ) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge - ) { - whenever(it.childCount).thenReturn(0) - } - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verify(mockDevLogHandler) - .handleLog( - Log.INFO, - GesturesListener.MSG_NO_TARGET_TAP - ) - verifyZeroInteractions(mockRumMonitor) - } - - @Test - fun `onTap keeps decorView as target if visible and clickable`(forge: Forge) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) { - whenever(it.childCount).thenReturn(0) - } - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(mockDecorView, expectedResourceName) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyMonitorCalledWithUserAction(mockDecorView, expectedResourceName) - } - - @Test - fun `onTap adds the target id hexa if NFE while requesting resource id`(forge: Forge) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - val targetId = forge.anInt() - val validTarget: View = mockView( - id = targetId, - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = false, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(validTarget) - } - whenever(mockResources.getResourceEntryName(validTarget.id)).thenThrow( - Resources.NotFoundException( - forge.anAlphabeticalString() - ) - ) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyMonitorCalledWithUserAction(validTarget, "0x${targetId.toString(16)}") - } - - @Test - fun `onTap adds the target id hexa when getResourceEntryName returns null`(forge: Forge) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - val targetId = forge.anInt() - val validTarget: View = mockView( - id = targetId, - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = false, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(validTarget) - } - whenever(mockResources.getResourceEntryName(validTarget.id)).thenReturn(null) - testedListener = GesturesListener( - WeakReference(mockWindow) - ) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyMonitorCalledWithUserAction(validTarget, "0x${targetId.toString(16)}") - } - - @Test - fun `will not send any span if decor view view reference is null`(forge: Forge) { - // Given - val mockEvent: MotionEvent = forge.getForgery() - testedListener = GesturesListener(WeakReference(null)) - - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verifyZeroInteractions(mockRumMonitor) - } - - @Test - fun `applies the extra attributes from the attributes providers`(forge: Forge) { - val mockEvent: MotionEvent = forge.getForgery() - val targetId = forge.anInt() - val validTarget: View = mockView( - id = targetId, - forEvent = mockEvent, - hitTest = true, - forge = forge, - clickable = true - ) - mockDecorView = mockDecorView( - id = forge.anInt(), - forEvent = mockEvent, - hitTest = false, - forge = forge - ) { - whenever(it.childCount).thenReturn(1) - whenever(it.getChildAt(0)).thenReturn(validTarget) - } - val expectedResourceName = forge.anAlphabeticalString() - mockResourcesForTarget(validTarget, expectedResourceName) - var expectedAttributes: MutableMap = mutableMapOf( - RumAttributes.ACTION_TARGET_CLASS_NAME to validTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName - ) - val providers = Array(forge.anInt(min = 0, max = 10)) { - mock { - whenever(it.extractAttributes(eq(validTarget), any())).thenAnswer { - val map = it.arguments[1] as MutableMap - map[forge.aString()] = forge.aString() - expectedAttributes = map - null - } - } - } - - testedListener = GesturesListener( - WeakReference(mockWindow), - providers - ) - // When - testedListener.onSingleTapUp(mockEvent) - - // Then - verify(mockRumMonitor).addUserAction( - RumActionType.TAP, - targetName(validTarget, expectedResourceName), - expectedAttributes - ) - } - - // region Internal - - private fun verifyMonitorCalledWithUserAction(target: View, expectedResourceName: String) { - verify(mockRumMonitor).addUserAction( - eq(RumActionType.TAP), - argThat { startsWith("${target.javaClass.simpleName}(") }, - argThat { - val targetClassName = target.javaClass.canonicalName - this[RumAttributes.ACTION_TARGET_CLASS_NAME] == targetClassName && - this[RumAttributes.ACTION_TARGET_RESOURCE_ID] == expectedResourceName - } - ) - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesUtilsTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesUtilsTest.kt deleted file mode 100644 index 0d8e4bc856..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesUtilsTest.kt +++ /dev/null @@ -1,111 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.app.Application -import android.content.res.Resources -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.lang.ref.WeakReference -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) -class GesturesUtilsTest { - - @Mock - lateinit var mockAppContext: Application - - @Mock - lateinit var mockResources: Resources - - @BeforeEach - fun `set up`() { - CoreFeature.contextRef = WeakReference(mockAppContext) - } - - @AfterEach - fun `tear down`() { - CoreFeature.contextRef = WeakReference(null) - } - - @Test - fun `it will return resource entry name if found`(forge: Forge) { - // Given - val resourceId = forge.anInt() - val resourceName = forge.aString() - whenever(mockAppContext.resources).thenReturn(mockResources) - whenever(mockResources.getResourceEntryName(resourceId)).thenReturn(resourceName) - - // When - assertThat(resourceIdName(resourceId)).isEqualTo(resourceName) - } - - @Test - fun `it will return the resource id as String hexa if Context resources are null`( - forge: Forge - ) { - // Given - val resourceId = forge.anInt() - whenever(mockAppContext.resources).thenReturn(null) - - // When - assertThat(resourceIdName(resourceId)) - .isEqualTo("0x${resourceId.toString(16)}") - } - - @Test - fun `it will return the resource id as String hexa if resource name could not be found`( - forge: Forge - ) { - // Given - val resourceId = forge.anInt() - whenever(mockAppContext.resources).thenReturn(mockResources) - whenever(mockResources.getResourceEntryName(resourceId)).thenThrow( - Resources.NotFoundException( - forge.aString() - ) - ) - - // When - assertThat(resourceIdName(resourceId)) - .isEqualTo("0x${resourceId.toString(16)}") - } - - @Test - fun `it will return the resource id as String hexa if resource name was null`( - forge: Forge - ) { - // Given - val resourceId = forge.anInt() - whenever(mockAppContext.resources).thenReturn(mockResources) - whenever(mockResources.getResourceEntryName(resourceId)).thenReturn(null) - - // When - assertThat(resourceIdName(resourceId)) - .isEqualTo("0x${resourceId.toString(16)}") - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/WindowCallbackWrapperTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/WindowCallbackWrapperTest.kt deleted file mode 100644 index 3b6d7df250..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/WindowCallbackWrapperTest.kt +++ /dev/null @@ -1,221 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.instrumentation.gestures - -import android.app.Application -import android.content.res.Resources -import android.view.KeyEvent -import android.view.MenuItem -import android.view.MotionEvent -import android.view.Window -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.RumMonitor -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.spy -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.lang.ref.WeakReference -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith( - MockitoExtension::class, - ForgeExtension::class - ) -) -@ForgeConfiguration(Configurator::class) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class WindowCallbackWrapperTest { - - lateinit var testedWrapper: WindowCallbackWrapper - - @Mock - lateinit var mockCallback: Window.Callback - - @Mock - lateinit var mockGestureDetector: GesturesDetectorWrapper - - @Mock - lateinit var mockAppContext: Application - - @Mock - lateinit var mockResources: Resources - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @BeforeEach - fun `set up`() { - testedWrapper = WindowCallbackWrapper( - mockCallback, - mockGestureDetector - ) - whenever(mockAppContext.resources).thenReturn(mockResources) - CoreFeature.contextRef = WeakReference(mockAppContext) - GlobalRum.registerIfAbsent(mockRumMonitor) - } - - @AfterEach - fun `tear down`() { - CoreFeature.contextRef = WeakReference(null) - GlobalRum.monitor = NoOpRumMonitor() - GlobalRum.isRegistered.set(false) - } - - @Test - fun `dispatchTouchEvent will delegate to wrapper`(forge: Forge) { - // Given - val motionEvent: MotionEvent = mock() - val spyTest = spy(testedWrapper) - val aBoolean = forge.aBool() - whenever(mockCallback.dispatchTouchEvent(motionEvent)).thenReturn(aBoolean) - doReturn(motionEvent).`when`(spyTest).copyEvent(motionEvent) - - // When - val returnedValue = spyTest.dispatchTouchEvent(motionEvent) - - // Then - assertThat(returnedValue).isEqualTo(aBoolean) - verify(mockCallback).dispatchTouchEvent(motionEvent) - } - - @Test - fun `dispatchTouchEvent will pass a copy of the event to the gesture detector`() { - // Given - val motionEvent: MotionEvent = mock() - val copyMotionEvent: MotionEvent = mock() - val spyTest = spy(testedWrapper) - doReturn(copyMotionEvent).`when`(spyTest).copyEvent(motionEvent) - - // When - spyTest.dispatchTouchEvent(motionEvent) - - // Then - verify(mockGestureDetector).onTouchEvent(copyMotionEvent) - verify(copyMotionEvent).recycle() - } - - @Test - fun `menu item selection will trigger a Rum UserActionEvent`(forge: Forge) { - // Given - val returnValue = forge.aBool() - val itemTitle = forge.aString() - val featureId = forge.anInt() - val itemId = forge.anInt() - val itemResourceName = forge.aString() - whenever(mockResources.getResourceEntryName(itemId)).thenReturn(itemResourceName) - val menuItem: MenuItem = mock { - whenever(it.itemId).thenReturn(itemId) - whenever(it.title).thenReturn(itemTitle) - } - whenever(mockCallback.onMenuItemSelected(featureId, menuItem)).thenReturn(returnValue) - - // When - assertThat(testedWrapper.onMenuItemSelected(featureId, menuItem)).isEqualTo(returnValue) - - // Then - inOrder(mockCallback, mockRumMonitor) { - verify(mockRumMonitor).addUserAction( - eq(RumActionType.TAP), - eq(targetName(menuItem, itemResourceName)), - argThat { - val targetClassName = menuItem.javaClass.canonicalName - this[RumAttributes.ACTION_TARGET_CLASS_NAME] == targetClassName && - this[RumAttributes.ACTION_TARGET_RESOURCE_ID] == itemResourceName && - this[RumAttributes.ACTION_TARGET_TITLE] == itemTitle - } - ) - verify(mockCallback).onMenuItemSelected(featureId, menuItem) - } - } - - @Test - fun `pressing back button will trigger specific user action event`(forge: Forge) { - // Given - val returnedValue = forge.aBool() - whenever(mockCallback.dispatchKeyEvent(any())).thenReturn(returnedValue) - val keyEvent = mockKeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK) - - // When - assertThat(testedWrapper.dispatchKeyEvent(keyEvent)).isEqualTo(returnedValue) - - // Then - inOrder(mockRumMonitor, mockCallback) { - verify(mockRumMonitor).addUserAction(RumActionType.CUSTOM, "back", emptyMap()) - verify(mockCallback).dispatchKeyEvent(keyEvent) - } - } - - @Test - fun `pressing back button will trigger user action event only on ACTION_UP`(forge: Forge) { - // Given - val returnedValue = forge.aBool() - whenever(mockCallback.dispatchKeyEvent(any())).thenReturn(returnedValue) - val keyEvent = mockKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK) - - // When - assertThat(testedWrapper.dispatchKeyEvent(keyEvent)).isEqualTo(returnedValue) - - // Then - inOrder(mockRumMonitor, mockCallback) { - verifyZeroInteractions(mockRumMonitor) - verify(mockCallback).dispatchKeyEvent(keyEvent) - } - } - - @Test - fun `pressing any other key except back button will do nothing`(forge: Forge) { - // Given - val returnedValue = forge.aBool() - whenever(mockCallback.dispatchKeyEvent(any())).thenReturn(returnedValue) - val keyCode = forge.anInt(min = 5) - val keyEvent = mockKeyEvent(KeyEvent.ACTION_UP, keyCode) - - // When - assertThat(testedWrapper.dispatchKeyEvent(keyEvent)).isEqualTo(returnedValue) - - // Then - inOrder(mockRumMonitor, mockCallback) { - verifyZeroInteractions(mockRumMonitor) - verify(mockCallback).dispatchKeyEvent(keyEvent) - } - } - - // region Internal - - private fun mockKeyEvent(action: Int, keyCode: Int): KeyEvent { - return mock { - whenever(it.keyCode).thenReturn(keyCode) - whenever(it.action).thenReturn(action) - } - } - - // endregion -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt deleted file mode 100644 index 5876ad4482..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt +++ /dev/null @@ -1,548 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.monitor - -import android.os.Handler -import com.datadog.android.core.internal.data.Writer -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.core.internal.net.FirstPartyHostDetector -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumResourceKind -import com.datadog.android.rum.internal.domain.event.ResourceTiming -import com.datadog.android.rum.internal.domain.event.RumEvent -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.domain.scope.RumApplicationScope -import com.datadog.android.rum.internal.domain.scope.RumRawEvent -import com.datadog.android.rum.internal.domain.scope.RumScope -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.forge.exhaustiveAttributes -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.datadog.tools.unit.setFieldValue -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.same -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.FloatForgery -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.UUID -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogRumMonitorTest { - - lateinit var testedMonitor: DatadogRumMonitor - - @Mock - lateinit var mockScope: RumScope - - @Mock - lateinit var mockWriter: Writer - - @Mock - lateinit var mockHandler: Handler - - @Mock - lateinit var mockDetector: FirstPartyHostDetector - - @Forgery - lateinit var fakeApplicationId: UUID - - lateinit var fakeAttributes: Map - - @FloatForgery(min = 0f, max = 100f) - var fakeSamplingRate: Float = 0f - - @BeforeEach - fun `set up`(forge: Forge) { - fakeAttributes = forge.exhaustiveAttributes() - testedMonitor = DatadogRumMonitor( - fakeApplicationId, - fakeSamplingRate, - mockWriter, - mockHandler, - mockDetector - ) - testedMonitor.setFieldValue("rootScope", mockScope) - } - - @AfterEach - fun `tear down`() { - } - - @Test - fun `creates root scope`() { - testedMonitor = DatadogRumMonitor( - fakeApplicationId, - fakeSamplingRate, - mockWriter, - mockHandler, - mockDetector - ) - - val rootScope = testedMonitor.rootScope - check(rootScope is RumApplicationScope) - assertThat(rootScope.samplingRate).isEqualTo(fakeSamplingRate) - } - - @Test - fun `M delegate event to rootScope W startView()`( - @StringForgery(type = StringForgeryType.ASCII) key: String, - @StringForgery name: String - ) { - testedMonitor.startView(key, name, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StartView - assertThat(event.key).isEqualTo(key) - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - assertThat(event.name).isEqualTo(name) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W stopView()`( - @StringForgery(type = StringForgeryType.ASCII) key: String - ) { - testedMonitor.stopView(key, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StopView - assertThat(event.key).isEqualTo(key) - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W addUserAction()`( - @Forgery type: RumActionType, - @StringForgery name: String - ) { - testedMonitor.addUserAction(type, name, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StartAction - assertThat(event.type).isEqualTo(type) - assertThat(event.name).isEqualTo(name) - assertThat(event.waitForStop).isFalse() - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W startUserAction()`( - @Forgery type: RumActionType, - @StringForgery name: String - ) { - testedMonitor.startUserAction(type, name, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StartAction - assertThat(event.type).isEqualTo(type) - assertThat(event.name).isEqualTo(name) - assertThat(event.waitForStop).isTrue() - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W stopUserAction()`( - @Forgery type: RumActionType, - @StringForgery name: String - ) { - testedMonitor.stopUserAction(type, name, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StopAction - assertThat(event.type).isEqualTo(type) - assertThat(event.name).isEqualTo(name) - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W startResource()`( - @StringForgery key: String, - @StringForgery method: String, - @RegexForgery("http(s?)://[a-z]+.com/[a-z]+") url: String - ) { - testedMonitor.startResource(key, method, url, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StartResource - assertThat(event.key).isEqualTo(key) - assertThat(event.method).isEqualTo(method) - assertThat(event.url).isEqualTo(url) - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W stopResource()`( - @StringForgery key: String, - @IntForgery(200, 600) statusCode: Int, - @LongForgery(0, 1024) size: Long, - @Forgery kind: RumResourceKind - ) { - testedMonitor.stopResource(key, statusCode, size, kind, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StopResource - assertThat(event.key).isEqualTo(key) - assertThat(event.statusCode).isEqualTo(statusCode.toLong()) - assertThat(event.kind).isEqualTo(kind) - assertThat(event.size).isEqualTo(size) - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W stopResource() {without status code nor size}`( - @StringForgery key: String, - @Forgery kind: RumResourceKind - ) { - testedMonitor.stopResource(key, null, null, kind, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StopResource - assertThat(event.key).isEqualTo(key) - assertThat(event.statusCode).isNull() - assertThat(event.kind).isEqualTo(kind) - assertThat(event.size).isNull() - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W stopResourceWithError()`( - @StringForgery key: String, - @StringForgery message: String, - @Forgery source: RumErrorSource, - @IntForgery(200, 600) statusCode: Int, - @Forgery throwable: Throwable - ) { - testedMonitor.stopResourceWithError(key, statusCode, message, source, throwable) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StopResourceWithError - assertThat(event.key).isEqualTo(key) - assertThat(event.statusCode).isEqualTo(statusCode.toLong()) - assertThat(event.message).isEqualTo(message) - assertThat(event.source).isEqualTo(source) - assertThat(event.throwable).isEqualTo(throwable) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W stopResourceWithError() {without status code}`( - @StringForgery key: String, - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - testedMonitor.stopResourceWithError(key, null, message, source, throwable) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.StopResourceWithError - assertThat(event.key).isEqualTo(key) - assertThat(event.statusCode).isNull() - assertThat(event.message).isEqualTo(message) - assertThat(event.source).isEqualTo(source) - assertThat(event.throwable).isEqualTo(throwable) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W addError`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - testedMonitor.addError(message, source, throwable, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.AddError - assertThat(event.message).isEqualTo(message) - assertThat(event.source).isEqualTo(source) - assertThat(event.throwable).isEqualTo(throwable) - assertThat(event.stacktrace).isNull() - assertThat(event.isFatal).isFalse() - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W onAddErrorWithStacktrace`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @StringForgery stacktrace: String - ) { - testedMonitor.addErrorWithStacktrace(message, source, stacktrace, fakeAttributes) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.AddError - assertThat(event.message).isEqualTo(message) - assertThat(event.source).isEqualTo(source) - assertThat(event.throwable).isNull() - assertThat(event.stacktrace).isEqualTo(stacktrace) - assertThat(event.isFatal).isFalse() - assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W viewTreeChanged()`() { - val eventTime = Time() - testedMonitor.viewTreeChanged(eventTime) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - check(firstValue is RumRawEvent.ViewTreeChanged) - assertThat(firstValue.eventTime).isEqualTo(eventTime) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W waitForResourceTiming()`( - @StringForgery key: String - ) { - testedMonitor.waitForResourceTiming(key) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue - check(event is RumRawEvent.WaitForResourceTiming) - assertThat(event.key).isEqualTo(key) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W addResourceTiming()`( - @StringForgery key: String, - @Forgery timing: ResourceTiming - ) { - testedMonitor.addResourceTiming(key, timing) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue - check(event is RumRawEvent.AddResourceTiming) - assertThat(event.key).isEqualTo(key) - assertThat(event.timing).isEqualTo(timing) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W addCustomTiming()`( - @StringForgery name: String - ) { - testedMonitor.addTiming(name) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue - check(event is RumRawEvent.AddCustomTiming) - assertThat(event.name).isEqualTo(name) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W addCrash()`( - @StringForgery message: String, - @Forgery source: RumErrorSource, - @Forgery throwable: Throwable - ) { - testedMonitor.addCrash(message, source, throwable) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue as RumRawEvent.AddError - assertThat(event.message).isEqualTo(message) - assertThat(event.source).isEqualTo(source) - assertThat(event.throwable).isEqualTo(throwable) - assertThat(event.isFatal).isTrue() - assertThat(event.attributes).isEmpty() - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W updateViewLoadingTime()`(forge: Forge) { - val key = forge.anAsciiString() - val loadingTime = forge.aLong(min = 1) - val loadingType = forge.aValueFrom(ViewEvent.LoadingType::class.java) - - testedMonitor.updateViewLoadingTime(key, loadingTime, loadingType) - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue - check(event is RumRawEvent.UpdateViewLoadingTime) - assertThat(event.key).isEqualTo(key) - assertThat(event.loadingTime).isEqualTo(loadingTime) - assertThat(event.loadingType).isEqualTo(loadingType) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `M delegate event to rootScope W resetSession()`() { - - testedMonitor.resetSession() - Thread.sleep(200) - - argumentCaptor { - verify(mockScope).handleEvent(capture(), same(mockWriter)) - - val event = firstValue - check(event is RumRawEvent.ResetSession) - } - verifyNoMoreInteractions(mockScope, mockWriter) - } - - @Test - fun `sends keep alive event to rootScope regularly`() { - argumentCaptor { - inOrder(mockScope, mockWriter, mockHandler) { - verify(mockHandler).postDelayed(capture(), eq(DatadogRumMonitor.KEEP_ALIVE_MS)) - verifyZeroInteractions(mockScope) - val runnable = firstValue - runnable.run() - Thread.sleep(200) - verify(mockHandler).removeCallbacks(same(runnable)) - verify(mockScope).handleEvent( - argThat { this is RumRawEvent.KeepAlive }, - same(mockWriter) - ) - verify(mockHandler).postDelayed(same(runnable), eq(DatadogRumMonitor.KEEP_ALIVE_MS)) - verifyNoMoreInteractions() - } - } - } - - @Test - fun `delays keep alive runnable on other event`() { - val mockEvent: RumRawEvent = mock() - val runnable = testedMonitor.keepAliveRunnable - - testedMonitor.handleEvent(mockEvent) - Thread.sleep(200) - - argumentCaptor { - inOrder(mockScope, mockWriter, mockHandler) { - verify(mockHandler).removeCallbacks(same(runnable)) - verify(mockScope).handleEvent(same(mockEvent), same(mockWriter)) - verify(mockHandler).postDelayed(same(runnable), eq(DatadogRumMonitor.KEEP_ALIVE_MS)) - verifyNoMoreInteractions() - } - } - } - - @Test - fun `removes callback from handler on stopKeepAliveCallback`() { - // initial post - verify(mockHandler).postDelayed(any(), any()) - - testedMonitor.stopKeepAliveCallback() - - verify(mockHandler).removeCallbacks(same(testedMonitor.keepAliveRunnable)) - verifyNoMoreInteractions(mockHandler, mockWriter, mockScope) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/net/RumOkHttpUploaderTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/net/RumOkHttpUploaderTest.kt deleted file mode 100644 index 4bb2a7d3b7..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/net/RumOkHttpUploaderTest.kt +++ /dev/null @@ -1,100 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.net - -import android.app.Application -import com.datadog.android.BuildConfig -import com.datadog.android.DatadogConfig -import com.datadog.android.core.internal.CoreFeature -import com.datadog.android.core.internal.net.DataOkHttpUploader -import com.datadog.android.core.internal.net.DataOkHttpUploaderTest -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.rum.RumAttributes -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.mockContext -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.RegexForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.concurrent.TimeUnit -import okhttp3.OkHttpClient -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class RumOkHttpUploaderTest : DataOkHttpUploaderTest() { - - @RegexForgery("([a-z]+\\.)+[a-z]+") - lateinit var fakePackageName: String - - @RegexForgery("\\d(\\.\\d){3}") - lateinit var fakePackageVersion: String - - @StringForgery - lateinit var fakeEnvName: String - lateinit var mockAppContext: Application - - @BeforeEach - override fun `set up`(forge: Forge) { - super.`set up`(forge) - RumFeature.envName = fakeEnvName - mockAppContext = mockContext(fakePackageName, fakePackageVersion) - CoreFeature.initialize( - mockAppContext, - forge.aValueFrom(TrackingConsent::class.java), - DatadogConfig.CoreConfig( - needsClearTextHttp = forge.aBool() - ) - ) - } - - @AfterEach - override fun `tear down`() { - super.`tear down`() - CoreFeature.stop() - } - - override fun uploader(): RumOkHttpUploader { - return RumOkHttpUploader( - fakeEndpoint, - fakeToken, - OkHttpClient.Builder() - .connectTimeout(TIMEOUT_TEST_MS, TimeUnit.MILLISECONDS) - .readTimeout(TIMEOUT_TEST_MS, TimeUnit.MILLISECONDS) - .writeTimeout(TIMEOUT_TEST_MS, TimeUnit.MILLISECONDS) - .build() - ) - } - - override fun urlFormat(): String { - return RumOkHttpUploader.UPLOAD_URL - } - - override fun expectedPathRegex(): String { - return "^\\/v1\\/input/$fakeToken" + - "\\?${DataOkHttpUploader.QP_BATCH_TIME}=\\d+" + - "&${DataOkHttpUploader.QP_SOURCE}=${DataOkHttpUploader.DD_SOURCE_ANDROID}" + - "&${RumOkHttpUploader.QP_TAGS}=" + - "${RumAttributes.SERVICE_NAME}:$fakePackageName," + - "${RumAttributes.APPLICATION_VERSION}:$fakePackageVersion," + - "${RumAttributes.SDK_VERSION}:${BuildConfig.VERSION_NAME}," + - "${RumAttributes.ENV}:$fakeEnvName" + - "$" - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/AndroidXFragmentLifecycleCallbacksTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/AndroidXFragmentLifecycleCallbacksTest.kt deleted file mode 100644 index 8892c57170..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/AndroidXFragmentLifecycleCallbacksTest.kt +++ /dev/null @@ -1,337 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.tracking - -import android.app.Dialog -import android.content.Context -import android.view.Window -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.fragment.app.FragmentManager -import com.datadog.android.core.internal.utils.resolveViewName -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.tracking.AcceptAllSupportFragments -import com.datadog.android.rum.tracking.ComponentPredicate -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(ForgeExtension::class), - ExtendWith(MockitoExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class AndroidXFragmentLifecycleCallbacksTest { - - lateinit var testedLifecycleCallbacks: AndroidXFragmentLifecycleCallbacks - - @Mock - lateinit var mockFragment: Fragment - - @Mock - lateinit var mockFragmentActivity: FragmentActivity - - @Mock - lateinit var mockFragmentManager: FragmentManager - - @Mock - lateinit var mockContext: Context - - @Mock - lateinit var mockWindow: Window - - @Mock - lateinit var mockDialog: Dialog - - @Mock - lateinit var mockGesturesTracker: GesturesTracker - - @Mock - lateinit var mockViewLoadingTimer: ViewLoadingTimer - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Mock - lateinit var mockAdvancedRumMonitor: AdvancedRumMonitor - - lateinit var fakeAttributes: Map - - @BeforeEach - fun `set up`(forge: Forge) { - RumFeature.gesturesTracker = mockGesturesTracker - - whenever(mockFragmentActivity.supportFragmentManager).thenReturn(mockFragmentManager) - fakeAttributes = forge.aMap { forge.aString() to forge.aString() } - testedLifecycleCallbacks = AndroidXFragmentLifecycleCallbacks( - { fakeAttributes }, - AcceptAllSupportFragments(), - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - } - - @Test - fun `when fragment attached, it will notify the timer`( - forge: Forge - ) { - testedLifecycleCallbacks.onFragmentAttached(mock(), mockFragment, mockFragmentActivity) - - verify(mockViewLoadingTimer).onCreated(mockFragment) - } - - @Test - fun `when fragment attached, and not whitelisted will not interact with timer`( - forge: Forge - ) { - testedLifecycleCallbacks = AndroidXFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - - testedLifecycleCallbacks.onFragmentAttached(mock(), mockFragment, mockFragmentActivity) - - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when fragment started, it will notify the timer`( - forge: Forge - ) { - testedLifecycleCallbacks.onFragmentStarted(mock(), mockFragment) - - verify(mockViewLoadingTimer).onStartLoading(mockFragment) - } - - @Test - fun `when fragment started, and not whitelisted will not interact with timer`( - forge: Forge - ) { - testedLifecycleCallbacks = AndroidXFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - testedLifecycleCallbacks.onFragmentStarted(mock(), mockFragment) - - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when fragment activity created on DialogFragment, it will register a Window Callback`( - forge: Forge - ) { - val mockDialogFragment: DialogFragment = mock() - whenever(mockDialogFragment.context) doReturn mockContext - whenever(mockDialogFragment.dialog) doReturn mockDialog - whenever(mockDialog.window) doReturn mockWindow - - testedLifecycleCallbacks.onFragmentActivityCreated(mock(), mockDialogFragment, null) - - verify(mockGesturesTracker).startTracking(mockWindow, mockContext) - } - - @Test - fun `when fragment activity created on Fragment, registers nothing`(forge: Forge) { - whenever(mockFragment.context) doReturn mockContext - - testedLifecycleCallbacks.onFragmentActivityCreated(mock(), mockFragment, null) - - verifyZeroInteractions(mockGesturesTracker) - } - - @Test - fun `when fragment resumed it will start a view event`(forge: Forge) { - // When - testedLifecycleCallbacks.onFragmentResumed(mock(), mockFragment) - // Then - verify(mockRumMonitor).startView( - eq(mockFragment), - eq(mockFragment.resolveViewName()), - eq(fakeAttributes) - ) - } - - @Test - fun `when fragment resumed, it will notify the timer and update the Rum event time`( - forge: Forge - ) { - val expectedLoadingTime = forge.aLong() - val firsTimeLoading = forge.aBool() - val expectedLoadingType = - if (firsTimeLoading) { - ViewEvent.LoadingType.FRAGMENT_DISPLAY - } else { - ViewEvent.LoadingType.FRAGMENT_REDISPLAY - } - whenever(mockViewLoadingTimer.getLoadingTime(mockFragment)) - .thenReturn(expectedLoadingTime) - whenever(mockViewLoadingTimer.isFirstTimeLoading(mockFragment)) - .thenReturn(firsTimeLoading) - testedLifecycleCallbacks.onFragmentResumed(mock(), mockFragment) - - verify(mockViewLoadingTimer).onFinishedLoading(mockFragment) - verify(mockAdvancedRumMonitor).updateViewLoadingTime( - mockFragment, - expectedLoadingTime, - expectedLoadingType - ) - } - - @Test - fun `when fragment resumed will do nothing if the fragment is not whitelisted`() { - // Given - testedLifecycleCallbacks = AndroidXFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - - // When - testedLifecycleCallbacks.onFragmentResumed(mock(), mockFragment) - - // Then - verifyZeroInteractions(mockViewLoadingTimer) - verifyZeroInteractions(mockRumMonitor) - verifyZeroInteractions(mockAdvancedRumMonitor) - } - - @Test - fun `when fragment paused it will mark the view as hidden in the timer`(forge: Forge) { - // When - testedLifecycleCallbacks.onFragmentPaused(mock(), mockFragment) - // Then - verify(mockRumMonitor).stopView( - eq(mockFragment), - eq(emptyMap()) - ) - - verify(mockViewLoadingTimer).onPaused(mockFragment) - } - - @Test - fun `when fragment paused it will stop a view event`(forge: Forge) { - // When - testedLifecycleCallbacks.onFragmentPaused(mock(), mockFragment) - // Then - verify(mockRumMonitor).stopView( - eq(mockFragment), - eq(emptyMap()) - ) - } - - @Test - fun `when fragment paused will do nothing if the fragment is not whitelisted`() { - // Given - testedLifecycleCallbacks = AndroidXFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - - // When - testedLifecycleCallbacks.onFragmentPaused(mock(), mockFragment) - - // Then - verifyZeroInteractions(mockRumMonitor) - verifyZeroInteractions(mockAdvancedRumMonitor) - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when fragment destroyed will remove view entry from timer`() { - // When - testedLifecycleCallbacks.onFragmentDestroyed(mock(), mockFragment) - - // Then - verify(mockViewLoadingTimer).onDestroyed(mockFragment) - } - - @Test - fun `when fragment destroyed and not whitelisted will do nothing`() { - // Given - testedLifecycleCallbacks = AndroidXFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - - // When - testedLifecycleCallbacks.onFragmentDestroyed(mock(), mockFragment) - - // Then - verifyZeroInteractions(mockRumMonitor) - verifyZeroInteractions(mockAdvancedRumMonitor) - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `will register the callback to fragment manager when required`() { - // When - testedLifecycleCallbacks.register(mockFragmentActivity) - - // Then - verify(mockFragmentManager) - .registerFragmentLifecycleCallbacks(testedLifecycleCallbacks, true) - } - - @Test - fun `will unregister the callback from the fragment manager when required`() { - // When - testedLifecycleCallbacks.unregister(mockFragmentActivity) - - // Then - verify(mockFragmentManager).unregisterFragmentLifecycleCallbacks(testedLifecycleCallbacks) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacksTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacksTest.kt deleted file mode 100644 index 01e9bc9b3c..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacksTest.kt +++ /dev/null @@ -1,375 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.tracking - -import android.app.Activity -import android.app.Dialog -import android.app.DialogFragment -import android.app.Fragment -import android.app.FragmentManager -import android.os.Build -import android.view.Window -import com.datadog.android.core.internal.utils.resolveViewName -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.internal.RumFeature -import com.datadog.android.rum.internal.domain.model.ViewEvent -import com.datadog.android.rum.internal.instrumentation.gestures.GesturesTracker -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.tracking.AcceptAllDefaultFragment -import com.datadog.android.rum.tracking.ComponentPredicate -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Suppress("DEPRECATION") -@Extensions( - ExtendWith(ForgeExtension::class), - ExtendWith(MockitoExtension::class), - ExtendWith(ApiLevelExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class OreoFragmentLifecycleCallbacksTest { - - lateinit var testedLifecycleCallbacks: OreoFragmentLifecycleCallbacks - - @Mock - lateinit var mockFragment: Fragment - - @Mock - lateinit var mockActivity: Activity - - @Mock - lateinit var mockWindow: Window - - @Mock - lateinit var mockDialog: Dialog - - @Mock - lateinit var mockFragmentManager: FragmentManager - - @Mock - lateinit var mockGesturesTracker: GesturesTracker - - @Mock - lateinit var mockViewLoadingTimer: ViewLoadingTimer - - @Mock - lateinit var mockRumMonitor: RumMonitor - - @Mock - lateinit var mockAdvancedRumMonitor: AdvancedRumMonitor - - lateinit var fakeAttributes: Map - - @BeforeEach - fun `set up`(forge: Forge) { - RumFeature.gesturesTracker = mockGesturesTracker - - whenever(mockActivity.fragmentManager).thenReturn(mockFragmentManager) - whenever(mockActivity.window).thenReturn(mockWindow) - - fakeAttributes = forge.aMap { forge.aString() to forge.aString() } - testedLifecycleCallbacks = OreoFragmentLifecycleCallbacks( - { fakeAttributes }, - AcceptAllDefaultFragment(), - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - } - - @AfterEach - fun `tear down`() { - GlobalRum.isRegistered.set(false) - GlobalRum.monitor = NoOpRumMonitor() - } - - @Test - fun `when fragment attached, it will notify the timer`( - forge: Forge - ) { - testedLifecycleCallbacks.onFragmentAttached(mock(), mockFragment, mockActivity) - - verify(mockViewLoadingTimer).onCreated(mockFragment) - } - - @Test - fun `when fragment attached, and not whitelisted will not interact with timer`( - forge: Forge - ) { - testedLifecycleCallbacks = OreoFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - testedLifecycleCallbacks.onFragmentAttached(mock(), mockFragment, mockActivity) - - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when fragment started, it will notify the timer`( - forge: Forge - ) { - testedLifecycleCallbacks.onFragmentStarted(mock(), mockFragment) - - verify(mockViewLoadingTimer).onStartLoading(mockFragment) - } - - @Test - fun `when fragment started, and not whitelisted will not interact with timer`( - forge: Forge - ) { - testedLifecycleCallbacks = OreoFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - testedLifecycleCallbacks.onFragmentStarted(mock(), mockFragment) - - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when fragment activity created on DialogFragment, it will register a Window Callback`( - forge: Forge - ) { - val mockDialogFragment: DialogFragment = mock() - whenever(mockDialogFragment.context) doReturn mockActivity - whenever(mockDialogFragment.dialog) doReturn mockDialog - whenever(mockDialog.window) doReturn mockWindow - - testedLifecycleCallbacks.onFragmentActivityCreated(mock(), mockDialogFragment, null) - - verify(mockGesturesTracker).startTracking(mockWindow, mockActivity) - } - - @Test - fun `when fragment activity created on Fragment, registers nothing`(forge: Forge) { - whenever(mockFragment.context) doReturn mockActivity - - testedLifecycleCallbacks.onFragmentActivityCreated(mock(), mockFragment, null) - - verifyZeroInteractions(mockGesturesTracker) - } - - @Test - fun `when fragment resumed it will start a view event`(forge: Forge) { - // When - testedLifecycleCallbacks.onFragmentResumed(mock(), mockFragment) - // Then - verify(mockRumMonitor).startView( - eq(mockFragment), - eq(mockFragment.resolveViewName()), - eq(fakeAttributes) - ) - } - - @Test - fun `when fragment resumed, it will notify the timer and update the Rum event time`( - forge: Forge - ) { - val expectedLoadingTime = forge.aLong() - val firsTimeLoading = forge.aBool() - val expectedLoadingType = - if (firsTimeLoading) { - ViewEvent.LoadingType.FRAGMENT_DISPLAY - } else { - ViewEvent.LoadingType.FRAGMENT_REDISPLAY - } - whenever(mockViewLoadingTimer.getLoadingTime(mockFragment)) - .thenReturn(expectedLoadingTime) - whenever(mockViewLoadingTimer.isFirstTimeLoading(mockFragment)) - .thenReturn(firsTimeLoading) - testedLifecycleCallbacks.onFragmentResumed(mock(), mockFragment) - - verify(mockViewLoadingTimer).onFinishedLoading(mockFragment) - verify(mockAdvancedRumMonitor).updateViewLoadingTime( - mockFragment, - expectedLoadingTime, - expectedLoadingType - ) - } - - @Test - fun `when fragment resumed will do nothing if the fragment is not whitelisted`() { - // Given - testedLifecycleCallbacks = OreoFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - - // When - testedLifecycleCallbacks.onFragmentResumed(mock(), mockFragment) - - // Then - verifyZeroInteractions(mockRumMonitor) - verifyZeroInteractions(mockAdvancedRumMonitor) - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when fragment paused it will mark the view as hidden in the timer`(forge: Forge) { - // When - testedLifecycleCallbacks.onFragmentPaused(mock(), mockFragment) - // Then - verify(mockRumMonitor).stopView( - eq(mockFragment), - eq(emptyMap()) - ) - - verify(mockViewLoadingTimer).onPaused(mockFragment) - } - - @Test - fun `when fragment paused it will stop a view event`(forge: Forge) { - // When - testedLifecycleCallbacks.onFragmentPaused(mock(), mockFragment) - // Then - verify(mockRumMonitor).stopView( - eq(mockFragment), - eq(emptyMap()) - ) - } - - @Test - fun `when fragment paused will do nothing if the fragment is not whitelisted`() { - // Given - testedLifecycleCallbacks = OreoFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - - // When - testedLifecycleCallbacks.onFragmentPaused(mock(), mockFragment) - - // Then - verifyZeroInteractions(mockRumMonitor) - verifyZeroInteractions(mockAdvancedRumMonitor) - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - fun `when fragment destroyed will remove view entry from timer`() { - // When - testedLifecycleCallbacks.onFragmentDestroyed(mock(), mockFragment) - - // Then - verify(mockViewLoadingTimer).onDestroyed(mockFragment) - } - - @Test - fun `when fragment destroyed and not whitelisted will do nothing`() { - // Given - testedLifecycleCallbacks = OreoFragmentLifecycleCallbacks( - { fakeAttributes }, - object : ComponentPredicate { - override fun accept(component: Fragment): Boolean { - return false - } - }, - viewLoadingTimer = mockViewLoadingTimer, - rumMonitor = mockRumMonitor, - advancedRumMonitor = mockAdvancedRumMonitor - ) - - // When - testedLifecycleCallbacks.onFragmentDestroyed(mock(), mockFragment) - - // Then - verifyZeroInteractions(mockRumMonitor) - verifyZeroInteractions(mockAdvancedRumMonitor) - verifyZeroInteractions(mockViewLoadingTimer) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `it will register the callback to fragment manager on O`() { - // When - testedLifecycleCallbacks.register(mockActivity) - - // Then - verify(mockFragmentManager).registerFragmentLifecycleCallbacks( - testedLifecycleCallbacks, - true - ) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.O) - fun `it will unregister the callback from fragment manager on O`() { - // When - testedLifecycleCallbacks.unregister(mockActivity) - - // Then - verify(mockFragmentManager).unregisterFragmentLifecycleCallbacks(testedLifecycleCallbacks) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.M) - fun `it will do nothing when calling register on M`() { - // When - testedLifecycleCallbacks.register(mockActivity) - - // Then - verifyZeroInteractions(mockFragmentManager) - } - - @Test - @TestTargetApi(Build.VERSION_CODES.M) - fun `it will do nothing when calling unregister on M`() { - // When - testedLifecycleCallbacks.unregister(mockActivity) - - // Then - verifyZeroInteractions(mockFragmentManager) - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/ViewLoadingTimerTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/ViewLoadingTimerTest.kt deleted file mode 100644 index 44bf4d224d..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/ViewLoadingTimerTest.kt +++ /dev/null @@ -1,198 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.tracking - -import com.nhaarman.mockitokotlin2.mock -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(ForgeExtension::class), - ExtendWith(MockitoExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -internal class ViewLoadingTimerTest { - - lateinit var testedTimer: ViewLoadingTimer - - @BeforeEach - fun `set up`() { - testedTimer = ViewLoadingTimer() - } - - @Test - fun `it returns the right time and view state first time the view is created`() { - // Given - val view: Any = mock() - testedTimer.onCreated(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - - // When - val loadingTime = testedTimer.getLoadingTime(view) - val firstTimeLoading = testedTimer.isFirstTimeLoading(view) - - // Then - assertThat(loadingTime).isGreaterThan(0) - assertThat(firstTimeLoading).isTrue() - } - - @Test - fun `it returns the right time and view state when the view is resumed from background`() { - // Given - val view: Any = mock() - testedTimer.onCreated(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - testedTimer.onPaused(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - - // When - val loadingTime = testedTimer.getLoadingTime(view) - val firstTimeLoading = testedTimer.isFirstTimeLoading(view) - - // Then - assertThat(loadingTime).isGreaterThan(0) - assertThat(firstTimeLoading).isFalse() - } - - @Test - fun `it returns the right time and view state when finishedLoading called multiple times`() { - // Given - val view: Any = mock() - testedTimer.onCreated(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - testedTimer.onFinishedLoading(view) - - // When - val loadingTime = testedTimer.getLoadingTime(view) - val firstTimeLoading = testedTimer.isFirstTimeLoading(view) - - // Then - assertThat(loadingTime).isGreaterThan(0) - assertThat(firstTimeLoading).isTrue() - } - - @Test - fun `at first launch it will compute the time between onCreate and onFinishedLoading`() { - // Given - val view: Any = mock() - testedTimer.onCreated(view) - Thread.sleep(500) // to simulate a long first time layout rendering - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - val loadingTimeFirstLaunch = testedTimer.getLoadingTime(view) - - // When - testedTimer.onPaused(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - val loadingTimeSecondLaunch = testedTimer.getLoadingTime(view) - - // Then - assertThat(loadingTimeFirstLaunch).isGreaterThan(loadingTimeSecondLaunch) - } - - @Test - fun `it returns the right time and state for different views in different states`() { - // Given - val view1: Any = mock() - val view2: Any = mock() - testedTimer.onCreated(view1) - testedTimer.onCreated(view2) - testedTimer.onStartLoading(view1) - testedTimer.onFinishedLoading(view1) - testedTimer.onStartLoading(view2) - testedTimer.onFinishedLoading(view2) - testedTimer.onPaused(view2) - testedTimer.onStartLoading(view2) - Thread.sleep(10) - testedTimer.onFinishedLoading(view2) - - // When - val loadingTimeView1 = testedTimer.getLoadingTime(view1) - val firstTimeLoadingView1 = testedTimer.isFirstTimeLoading(view1) - val loadingTimeView2 = testedTimer.getLoadingTime(view2) - val firstTimeLoadingView2 = testedTimer.isFirstTimeLoading(view2) - - // Then - assertThat(loadingTimeView1).isGreaterThan(0) - assertThat(firstTimeLoadingView1).isTrue() - assertThat(loadingTimeView2).isGreaterThan(0) - assertThat(firstTimeLoadingView2).isFalse() - assertThat(loadingTimeView2).isGreaterThan(loadingTimeView1) - } - - @Test - fun `it returns false for firstTimeLoading if the view was resumed without being started`() { - // Given - val view: Any = mock() - testedTimer.onCreated(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - testedTimer.onPaused(view) - testedTimer.onFinishedLoading(view) - - // When - val loadingTime = testedTimer.getLoadingTime(view) - val firstTimeLoading = testedTimer.isFirstTimeLoading(view) - - // Then - assertThat(loadingTime).isEqualTo(0) - assertThat(firstTimeLoading).isFalse() - } - - @Test - fun `it will return false for firstTimeLoading after the view was hidden`() { - // Given - val view: Any = mock() - testedTimer.onCreated(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - testedTimer.onPaused(view) - testedTimer.onFinishedLoading(view) - - // When - val loadingTime = testedTimer.getLoadingTime(view) - val firstTimeLoading = testedTimer.isFirstTimeLoading(view) - - // Then - assertThat(loadingTime).isEqualTo(0) - assertThat(firstTimeLoading).isFalse() - } - - @Test - fun `it clear the references if the view is destroyed`() { - // Given - val view: Any = mock() - testedTimer.onCreated(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - testedTimer.onPaused(view) - testedTimer.onStartLoading(view) - testedTimer.onFinishedLoading(view) - testedTimer.onPaused(view) - testedTimer.onDestroyed(view) - - // When - val loadingTime = testedTimer.getLoadingTime(view) - val firstTimeLoading = testedTimer.isFirstTimeLoading(view) - - // Then - assertThat(loadingTime).isNull() - assertThat(firstTimeLoading).isFalse() - } -} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/ViewTreeChangeTrackingStrategyTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/ViewTreeChangeTrackingStrategyTest.kt deleted file mode 100644 index f921538374..0000000000 --- a/dd-sdk-android/src/test/kotlin/com/datadog/android/rum/internal/tracking/ViewTreeChangeTrackingStrategyTest.kt +++ /dev/null @@ -1,142 +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 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.tracking - -import android.app.Activity -import android.view.View -import android.view.ViewTreeObserver -import android.view.Window -import com.datadog.android.core.internal.domain.Time -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.NoOpRumMonitor -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.utils.forge.Configurator -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions -import com.nhaarman.mockitokotlin2.whenever -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class ViewTreeChangeTrackingStrategyTest { - - lateinit var testedStrategy: ViewTreeChangeTrackingStrategy - - @Mock - lateinit var mockActivity: Activity - - @Mock - lateinit var mockViewTreeObserver: ViewTreeObserver - - @Mock - lateinit var mockWindow: Window - - @Mock - lateinit var mockDecorView: View - - @Mock - lateinit var mockRumMonitor: AdvancedRumMonitor - - @BeforeEach - fun `set up`() { - - GlobalRum.registerIfAbsent(mockRumMonitor) - - whenever(mockActivity.window) doReturn mockWindow - whenever(mockWindow.decorView) doReturn mockDecorView - whenever(mockDecorView.viewTreeObserver) doReturn mockViewTreeObserver - - testedStrategy = ViewTreeChangeTrackingStrategy() - } - - @AfterEach - fun `tear down`() { - GlobalRum.isRegistered.set(false) - GlobalRum.monitor = NoOpRumMonitor() - } - - @Test - fun `𝕄 add listener 𝕎 onActivityStarted()`() { - // Given - - // When - testedStrategy.onActivityStarted(mockActivity) - - // Then - verify(mockViewTreeObserver).addOnGlobalLayoutListener(testedStrategy) - } - - @Test - fun `𝕄 doNothing 𝕎 onActivityStarted() without window`() { - // Given - whenever(mockActivity.window) doReturn null - - // When - testedStrategy.onActivityStarted(mockActivity) - - // Then - verifyZeroInteractions(mockViewTreeObserver) - } - - @Test - fun `𝕄 remove listener 𝕎 onActivityStopped()`() { - // Given - - // When - testedStrategy.onActivityStopped(mockActivity) - - // Then - verify(mockViewTreeObserver).removeOnGlobalLayoutListener(testedStrategy) - } - - @Test - fun `𝕄 doNothing 𝕎 onActivityStopped() without window`() { - // Given - whenever(mockActivity.window) doReturn null - - // When - testedStrategy.onActivityStopped(mockActivity) - - // Then - verifyZeroInteractions(mockViewTreeObserver) - } - - @Test - fun `𝕄 send viewTreeChanged event 𝕎 onGlobalLayout()`() { - // Given - - // When - val before = Time() - testedStrategy.onGlobalLayout() - val after = Time() - - // Then - argumentCaptor

    The Android NDK module is in public alpha and not supported by Datadog.
    - -Send crash report for issues rising from the C/C++ code in your application. - -**Note**: This package is an extension of the main package, so add both dependencies into your gradle file. - -## Setup - -```conf -repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } -} - -dependencies { - implementation "com.datadoghq:dd-sdk-android:x.x.x" - implementation "com.datadoghq:dd-sdk-android-ndk:x.x.x" -} -``` - -### Plugins - -1. The NDKCrashReporterPlugin handles the crash signals at the native level and reports them as errors by adding more explicit information (for example: backtrace, signal name, signal relevant error message) in the Datadog logs dashboard: - -```kotlin - -val config = DatadogConfig.Builder("", "", "") - .addPlugin(NDKCrashReporterPlugin(), Feature.CRASH) - .build() -Datadog.initialize(this, config) - -``` diff --git a/docs/rum_collection.md b/docs/rum_collection.md deleted file mode 100644 index 70550e9c75..0000000000 --- a/docs/rum_collection.md +++ /dev/null @@ -1,288 +0,0 @@ -# Android RUM Collection - -Send [Real User Monitoring data][1] to Datadog from your Android applications with [Datadog's `dd-sdk-android` client-side RUM library][2] and leverage the following features: - -* get a global idea about your app’s performance and demographics; -* understand which resources are the slowest; -* analyze errors by OS and device type. - -## Setup - -1. Add the Gradle dependency by declaring the library as a dependency in your `build.gradle` file: - - ```conf - repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } - } - - dependencies { - implementation "com.datadoghq:dd-sdk-android:x.x.x" - } - ``` - -2. Initialize the library with your application context and your [Datadog client token][4]. For security reasons, you must use a client token: you cannot use [Datadog API keys][5] to configure the `dd-sdk-android` library as they would be exposed client-side in the Android application APK byte code. For more information about setting up a client token, see the [client token documentation][4]. You also need to provide an Application ID (see our [RUM Getting Started page][3]). - - {{< tabs >}} - {{% tab "US" %}} - -```kotlin -class SampleApplication : Application() { - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "") - .trackInteractions() - .useViewTrackingStrategy(strategy) - .build() - Datadog.initialize(this, config) - } -} -``` - - {{% /tab %}} - {{% tab "EU" %}} - -```kotlin -class SampleApplication : Application() { - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "") - .trackInteractions() - .useViewTrackingStrategy(strategy) - .useEUEndpoints() - .build() - Datadog.initialize(this, config) - } -} -``` - - {{% /tab %}} - {{< /tabs >}} - -Depending on your application's architecture, you can choose one of several implementations of `ViewTrackingStrategy`: - - - `ActivityViewTrackingStrategy`: Every activity in your application is considered a distinct view. - - `FragmentViewTrackingStrategy`: Every fragment in your application is considered a distinct view. - - `NavigationViewTrackingStrategy`: If you use the Android Jetpack Navigation library, this is the recommended strategy. It automatically tracks the navigation destination as a distinct view. - - `MixedViewTrackingStrategy`: Every activity or fragment in your application is considered a distinct view. This strategy is a mix between the `ActivityViewTrackingStrategy` and `FragmentViewTrackingStrategy`. - - **Note**: For `ActivityViewTrackingStrategy`, `FragmentViewTrackingStrategy`, or `MixedViewTrackingStrategy` you can filter which `Fragment` or `Activity` is tracked as a RUM View by providing a `ComponentPredicate` implementation in the constructor. - - **Note**: By default, the library won't track any view. If you decide not to provide a view tracking strategy you will have to manually send the views by calling the `startView` and `stopView` methods yourself. - -3. Configure and register the RUM Monitor. You only need to do it once, usually in your application's `onCreate()` method: - - ```kotlin - val monitor = RumMonitor.Builder() - // Optionally set a sampling between 0.0 and 100.0% - // Here 75% of the RUM Sessions will be sent to Datadog - .sampleRumSessions(75.0f) - .build() - GlobalRum.registerIfAbsent(monitor) - ``` - -4. If you want to track your OkHttp requests as resources, you can add the provided [Interceptor][6] as follows: - - ```kotlin - val okHttpClient = OkHttpClient.Builder() - .addInterceptor(DatadogInterceptor()) - .build() - ``` - - This creates RUM Resource data around each request processed by the OkHttpClient, with all the relevant information automatically filled (URL, method, status code, error). Note that only network requests started when a view is active will be tracked. If you want to track requests when your application is in the background, you can create a view manually, as explained below. - - **Note**: If you also use multiple Interceptors, this one must be called first. - -5. (Optional) If you want to get timing information in Resources (such as time to first byte, DNS resolution, etc.), you can add the [Event][6] listener factory as follows: - - ```kotlin - val okHttpClient = OkHttpClient.Builder() - .addInterceptor(DatadogInterceptor()) - .eventListenerFactory(DatadogEventListener.Factory()) - .build() - ``` - -6. (Optional) If you want to manually track RUM events, you can use the `GlobalRum` class. - - To track views, call the `RumMonitor#startView` when the view becomes visible and interactive (equivalent with the lifecycle event `onResume`) followed by `RumMonitor#stopView` when the view is no longer visible(equivalent with the lifecycle event `onPause`) as follows: - - ```kotlin - fun onResume(){ - GlobalRum.get().startView(viewKey, viewName, viewAttributes) - } - - fun onPause(){ - GlobalRum.get().stopView(viewKey, viewAttributes) - } - ``` - - To track resources, call the `RumMonitor#startResource` when the resource starts being loaded, and `RumMonitor#stopResource` when it is fully loaded, or `RumMonitor#stopResourceWithError` if an error occurs while loading the resource, as follows: - - ```kotlin - fun loadResource(){ - GlobalRum.get().startResource(resourceKey, method, url, resourceAttributes) - try { - // do load the resource - GlobalRum.get().stopResource(resourceKey, resourceKind, additionalAttributes) - } catch (e : Exception) { - GlobalRum.get().stopResourceWithError(resourceKey, message, origin, e) - } - } - ``` - - To track user actions, call the `RumMonitor#addUserAction`, or for continuous actions, call the `RumMonitor#startUserAction` and `RumMonitor#stopUserAction`, as follows: - - ```kotlin - fun onUserInteraction(){ - GlobalRum.get().addUserAction(resourceKey, method, url, resourceAttributes) - } - ``` - -7. (Optional) If you want to add custom information as attributes to all RUM events, you can use the `GlobalRum` class. - - ```kotlin - // Adds an attribute to all future RUM events - GlobalRum.addAttribute(key, value) - - // Removes an attribute to all future RUM events - GlobalRum.removeAttribute(key) - ``` - -## Advanced logging - -### Library Initialization - -The following methods in `DatadogConfig.Builder` can be used when creating the Datadog Configuration to initialize the library: - -| Method | Description | -|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `setServiceName()` | Set `` as default value for the `service` [standard attribute][4] attached to all logs sent to Datadog (this can be overriden in each Logger). | -| `setRumEnabled(true)` | Set to `true` to enable sending RUM data to Datadog. | -| `trackInteractions(Array)` | Enables tracking User interactions (such as Tap, Scroll or Swipe). The parameter allow you to add custom attributes to the RUM Action events based on the widget with which the user interacted. | -| `useViewTrackingStrategy(strategy)` | Defines the strategy used to track Views. Depending on your application's architecture, you can choose one of several implementations of `ViewTrackingStrategy` (see above) or implement your own. | -| `addPlugin(DatadogPlugin, Feature)` | Adds a plugin implementation for a specific feature (CRASH, LOG, TRACE, RUM). The plugin will be registered once the feature is initialized and unregistered when the feature is stopped. | - -### RumMonitor Initialization - -The following methods in `RumMonitor.Builder` can be used when creating the RumMonitor to track RUM data: - -| Method | Description | -|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `sampleRumSessions(float)` | Sets the sampling rate for RUM Sessions. This method expects a value between 0 and 100, and is used as a percentage of Session for which data will be sent to Datadog. | - -### Manual Tracking - -If you need to manually track events, you can do so by getting the active `RumMonitor` instance, and call one of the following methods: - -| Method | Description | -|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `startView(, , )` | Notifies the RumMonitor that a new View just started. Most often, this method should be called in the frontmost `Activity` or `Fragment`'s `onResume()` method. | -| `stopView(, )` | Notifies the RumMonitor that the current View just stopped. Most often, this method should be called in the frontmost `Activity` or `Fragment`'s `onPause()` method. | -| `addUserAction(, , )` | Notifies the RumMonitor that a user action just happened. | -| `startUserAction(, , )` | Notifies the RumMonitor that a continuous user action just started (for example a user scrolling a list). | -| `stopUserAction(, , )` | Notifies the RumMonitor that a continuous user action just stopped. | -| `startResource(, , , )` | Notifies the RumMonitor that the application started loading a resource with a given method (e.g.: `GET` or `POST`), at the given url. | -| `stopResource(, , , )` | Notifies the RumMonitor that a resource finished being loaded, with a given status (usually an HTTP status code), size (in bytes) and kind. | -| `stopResourceWithError(, , , , )` | Notifies the RumMonitor that a resource couldn't finished being loaded, because of an exception. | -| `addError(, , , )` | Notifies the RumMonitor that an error occurred. | - - -### Tracking widgets - -Most of the time, the widgets are displayed in the `AppWidgetHostView` provided by the HomeScreen application, and we are not -able to provide auto-instrumentation for those components. To send UI interaction information from your widgets, manually call our -API. See one example approach in this sample application: -[Tracking widgets](https://github.com/DataDog/dd-sdk-android/tree/master/sample/kotlin/src/main/kotlin/com/datadog/android/sample/widget) - -## Batch collection - -All the RUM events are first stored on the local device in batches. Each batch follows the intake specification. They are sent as soon as the network is available, and the battery is high enough to ensure the Datadog SDK does not impact the end user's experience. If the network is not available while your application is in the foreground, or if an upload of data fails, the batch is kept until it can be sent successfully. - -This means that even if users open your application while being offline, no data will be lost. - -The data on the disk will automatically be discarded if it gets too old to ensure the SDK doesn't use too much disk space. - -## Extensions - -### Coil - -If you use Coil to load images in your application, take a look at Datadog's [dedicated library](https://github.com/DataDog/dd-sdk-android/tree/master/dd-sdk-android-coil). - -### Fresco - -If you use Fresco to load images in your application, take a look at Datadog's [dedicated library](https://github.com/DataDog/dd-sdk-android/tree/master/dd-sdk-android-fresco). - -### Glide - -If you use Glide to load images in your application, take a look at our [dedicated library](https://github.com/DataDog/dd-sdk-android/tree/master/dd-sdk-android-glide). - -### Picasso - -If you use Picasso, let it use your `OkHttpClient`, and you'll get RUM and APM information about network requests made by Picasso. - -```kotlin - val picasso = Picasso.Builder(context) - .downloader(OkHttp3Downloader(okHttpClient)) - // … - .build() - Picasso.setSingletonInstance(picasso) -``` - -### Retrofit - -If you use Retrofit, let it use your `OkHttpClient`, and you'll get RUM and APM information about network requests made with Retrofit. - -```kotlin - val retrofitClient = Retrofit.Builder() - .client(okHttpClient) - // … - .build() -``` - -### SQLDelight - -If you use SQLDelight, take a look at our [dedicated library](https://github.com/DataDog/dd-sdk-android/tree/master/dd-sdk-android-sqldelight). - -### SQLite - -Following SQLiteOpenHelper's [Generated API documentation][8], you only have to provide the implementation of the -DatabaseErrorHandler -> `DatadogDatabaseErrorHandler` in the constructor. - -Doing this detects whenever a database is corrupted and sends a relevant -RUM error event for it. - -```kotlint - class : SqliteOpenHelper(, - , - , - , - DatadogDatabaseErrorHandler()) { - // … - - } -``` - -### Apollo (GraphQL) - -If you use Apollo, let it use your `OkHttpClient`, and you'll get RUM and APM information about all the queries performed through Apollo client. - -```kotlin - val apolloClient = ApolloClient.builder() - .okHttpClient(okHttpClient) - .serverUrl() - .build() -``` - -## Further Reading - -{{< partial name="whats-next/whats-next.html" >}} - -[1]: https://docs.datadoghq.com/real_user_monitoring/data_collected/ -[2]: https://github.com/DataDog/dd-sdk-android -[3]: https://docs.datadoghq.com/real_user_monitoring/installation/?tab=us -[4]: https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens -[5]: https://docs.datadoghq.com/account_management/api-app-keys/#api-keys -[6]: https://square.github.io/okhttp/interceptors/ -[7]: https://square.github.io/okhttp/events/ -[8]: https://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper diff --git a/docs/sdk_performance.md b/docs/sdk_performance.md index fa1f84e163..4f8a77a9c4 100644 --- a/docs/sdk_performance.md +++ b/docs/sdk_performance.md @@ -1,30 +1,141 @@ -# SDK Performance +# SDK Performance and impact on the host application -## SDK Size +## Methodology -**The SDK is 723 KB**, measured as the `.aar` package size -without the transitive dependencies. The SDK uses the following transitive dependencies: +To simulate the typical usage of Datadog SDK it was added to the [Docile-Alligator/Infinity-For-Reddit][1] application and typical user behavior (scrolling the feed, browsing reddits) was simulated for 2 minutes 30 seconds. - - androidx.core:core - - androidx.navigation:navigation-fragment-ktx - - androidx.navigation:navigation-ui-ktx - - androidx.navigation:navigation-runtime-ktx - - androidx.recyclerview:recyclerview - - androidx.work:work-runtime - - com.google.code.gson:gson - - com.lyft.kronos:kronos-android - - com.squareup.okhttp3:okhttp - - org.jetbrains.kotlin:kotlin-stdlib +Application used: [Docile-Alligator/Infinity-For-Reddit][1] (revision [e8c9915a](https://github.com/Docile-Alligator/Infinity-For-Reddit/tree/e8c9915a)) +Device used: Google Pixel 6 +Android OS: Android 13 (Build Number TQ2A.230505.002) +Datadog SDK: revision [7f842d343](https://github.com/DataDog/dd-sdk-android/tree/7f842d343) -## Background behavior +Network profiling was done using Charles Proxy 4.6.4. +CPU, Memory, Energy profiling was done using Android Studio Flamingo | 2022.2.1 Patch 1. +Device had 1.6 GB memory free on average (out of 7.6 GB), 115 apps installed and 25 GB of storage was free. +Device was connected to the 4G network, WiFi interface was disabled. + +CPU and Memory profiling was done for the minified `release` build type with `profileable` attribute. CPU profiling was done using the `System Traces` option. + +SDK modules which were added to the application: + +* `dd-sdk-android-logs` +* `dd-sdk-android-trace` +* `dd-sdk-android-rum` +* `dd-sdk-android-okhttp` +* `dd-sdk-android-glide` + +SDK was set up with default settings. + +## Performance profiling + +### Network traffic + +| Measurement | Traffic | +|-------------|----------------------------------------------------------------------| +| #1 | 23 requests: 62.2 KB sent, 22.2 KB received. | +| #2 | 29 requests: 72.5 KB sent, 23.2 KB received. | +| #3 | 28 requests: 86.1 KB sent, 23.2 KB received. | +| #4 | 27 requests: 71.9 KB sent, 23.1 KB received. | +| #5 | 25 requests: 69.8 KB sent, 22.7 KB received. | +| average | 26 requests: 72.5 KB (σ=7.73 KB) sent, 22.9 KB (σ=0.39 KB) received. | + +All requests were sent using `gzip` encoding, compression was around 90%. + +For each measurement round the first request had an initial TLS handshake with certificate chain download, which had a size of 17.5 KB. Removing this single server certificate chain download from calculation leads to the following results. + +| Measurement | Traffic | +|-------------|---------------------------------------------------------------------| +| #1 | 23 requests: 62.2 KB sent, 4.7 KB received. | +| #2 | 29 requests: 72.5 KB sent, 5.7 KB received. | +| #3 | 28 requests: 86.1 KB sent, 5.7 KB received. | +| #4 | 27 requests: 71.9 KB sent, 5.6 KB received. | +| #5 | 25 requests: 69.8 KB sent, 5.2 KB received. | +| average | 26 requests: 72.5 KB sent (σ=7.73 KB), 5.4 KB (σ=0.39 KB) received. | + +### Peak CPU and Memory usage + +**Note**: Measurement without SDK means the measurement of the original application without Datadog SDK added as a dependency. + +| Measurement | CPU with SDK | CPU w/o SDK | Memory with SDK | Memory w/o SDK | +|-------------|-----------------|-----------------|-----------------------|--------------------| +| #1 | 19% | 23% | 432 MB | 413 MB | +| #2 | 32% | 23% | 441 MB | 470 MB | +| #3 | 25% | 27% | 400 MB | 430 MB | +| #4 | 32% | 29% | 453 MB | 432 MB | +| #5 | 26% | 24% | 437 MB | 440 MB | +| average | 26.8% (σ=4.87%) | 25.2% (σ=2.4%) | 432.6 MB (σ=17.72 MB) | 437 MB (σ=18.7 MB) | + +CPU usage pattern was the same, with the majority of the CPU usage below 10% during user interactions with the app and around 1-2% when application was in idle (no interactions performed). +Memory allocation/de-allocation pattern stays the same with and without SDK. + +### Janky frames + +Janky frames are described in the [official Android documentation][2] + +| Measurement | With SDK | Without SDK | +|-------------|-----------------|-----------------| +| #1 | 57/5883 (0.9%) | 69/7781 (0.9%) | +| #2 | 73/6580 (1.1%) | 43/6247 (0.7%) | +| #3 | 57/6323 (0.9%) | 81/6607 (1.2%) | +| #4 | 59/5628 (1.0%) | 81/7688 (1.0%) | +| #5 | 62/6256 (0.9%) | 62/6577 (0.9%) | +| average | 0.96% (σ=0.08%) | 0.94% (σ=0.16%) | + +Datadog SDK doesn't have any meaningful impact on the amount of janky frames in the app. + +### Energy consumption + +Documentation about this metric can be found [here][3]. + +| Measurement | With SDK | Without SDK | +|-------------|----------|-------------| +| #1 | LIGHT | LIGHT | +| #2 | LIGHT | LIGHT | +| #3 | LIGHT | LIGHT | +| #4 | LIGHT | LIGHT | +| #5 | LIGHT | LIGHT | + +### Application Startup Time + +As a reference we measured [Time To Initial Display][4]. + +Cold start was simulated by running the following command (application was killed before run): + +```shell +adb shell am start -S -W ml.docilealligator.infinityforreddit/.activities.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN +``` + +| Measurement | With SDK | Without SDK | +|-------------|-------------------|---------------------| +| #1 | 246 ms | 228 ms | +| #2 | 239 ms | 222 ms | +| #3 | 242 ms | 233 ms | +| #4 | 241 ms | 233 ms | +| #5 | 247 ms | 228 ms | +| average | 243 ms (σ=3.4 ms) | 228.8 ms (σ=4.5 ms) | + +SDK and its initialization has no significant impact on the `Time To Initial Display` metric. + +### Application size impact + +The measurement was done for the `2.0.0-beta2` version of Datadog SDK for the `minifiedRelease` application variant. + +`apk` size without Datadog SDK: 11044045 bytes +`apk` size with Datadog SDK: 11566506 bytes + +Datadog SDK added 552 KB to the `apk` size. + +## SDK behavior in the host application + +### Background behavior When the application goes in background: - the auto-instrumentation stops and detaches itself from any UI callbacks (lifecycle events, gesture detection). No RUM event will be automatically tracked but you can still manually send them with the `RumMonitor`. - - The endpoint accepts logs and traces as usual. + - The endpoints accept RUM events, logs and traces as usual. - The endpoints collect and send new batches of data. -## How batches are created and sent +### How batches are created and sent When a new event is ready to be serialized and batched, the persistence layer asks for the last known batch file to store the serialized event. A batch file is valid for appending new data when all the following conditions are met: @@ -38,16 +149,141 @@ valid for appending new data when all the following conditions are met: linearly with every batch sent. The maximum frequency is one batch per second. If there's no batch or network available or the battery level is too low, the upload frequency is linearly decreased to the minimum default value of one batch every 20 seconds. -## Battery consumption +### Battery consumption The SDK does not perform network activity if the device battery level is less than 10% or if the device is in power saving mode. -## Lifespan of the persisted data +### Lifespan of the persisted data The SDK stores data in batches and tries to send those whenever the network is available. A batch will not be stored more than 18 hours in an application. Every time the SDK reads a new batch for sending, will first remove batches that are older than 18 hours. -## Low available storage +### Low available storage The SDK checks the storage space used every time it creates a new batch. If this value is greater than 512 MB (the maximum amount of storage space that the SDK will use), it first tries to make more space available -by removing the older files. +by removing the older files. + +[1]: https://github.com/Docile-Alligator/Infinity-For-Reddit +[2]: https://developer.android.com/studio/profile/jank-detection +[3]: https://developer.android.com/studio/profile/energy-profiler +[4]: https://developer.android.com/topic/performance/vitals/launch-time#time-initial + +## Session Relay Performance Measurement + +The Session Replay feature is expected to have a larger impact on both CPU and memory usage +within the application. + +This section provides detailed performance measurements across different scenarios. + +### Methodology + +To simulate the typical usage of the Datadog SDK with Session Replay enabled, measurements were +performed using two applications: + +Application used: [Docile-Alligator/Infinity-For-Reddit][1] ( +revision [cfe1781](https://github.com/Docile-Alligator/Infinity-For-Reddit/tree/cfe1781)) for +Android View +[NowInAndroid](https://github.com/android/nowinandroid) (revision [ +904e6fccee809556b242898fb624f5f38200298c)) for Jetpack Compose + +Device used: Samsung Galaxy S23 +Android OS: Android 14 (Build Number UP1A.231005.007) +Datadog SDK: revision [2.17.0](https://github.com/DataDog/dd-sdk-android/tree/2.17.0) + +CPU, Memory profiling was done using Android Studio Ladybug | 2024.2.1 Patch 2. +Device had 3.7 GB memory free on average (out of 8 GB), 110 apps installed and 41.7 GB of storage +was free. +Device was connected to WiFi interface. + +CPU and Memory profiling was done for the debug build type. CPU profiling was done using the System +Traces option. + +Each measurement lasted for 1 minute, during which typical user behavior was simulated (scrolling +the feed, browsing content). + +### Scenario configuration + +In the following measurements, "Baseline" refers to the scenario where the application integrates +the Datadog SDK without enabling the Session Replay feature. + +The table below outlines the detailed masking configurations used in each measurement scenario: + +| Measurement | Touch Privacy | Text & Input | Image | +|-------------------|---------------|-----------------------|-----------| +| Minimum recording | HIDE | MASK_ALL | MASK_ALL | +| Touch Only | SHOW | MASK_ALL | MASK_ALL | +| Text & Input Only | HIDE | MASK_SENSITIVE_INPUTS | MASK_ALL | +| Image Only | HIDE | MASK_ALL | MASK_NONE | +| All | SHOW | MASK_SENSITIVE_INPUTS | MASK_NONE | + +### CPU Peak + +#### Android View + +| Measurement | Baseline | Minimum recording | Touch Only | Text & Input Only | Image Only | All | +|-------------|------------------|-------------------|------------------|-------------------|------------------|------------------| +| #1 | 27.9% | 26% | 25.5% | 25.4% | 32.3% | 23.8% | +| #2 | 21.7% | 24.2% | 30.3% | 32.9% | 29.2% | 34.6% | +| #3 | 27.8% | 27.3% | 22.7% | 28.8% | 36.1% | 28.6% | +| #4 | 30.8% | 21.4% | 29.4% | 23.2% | 29.4% | 25.8% | +| #5 | 24.2% | 29.7% | 24.4% | 26.2% | 41% | 32.3% | +| Average | 26.48%<(σ=3.18%) | 25.72% (σ=2.81%) | 26.46% (σ=2.92%) | 27.30% (σ=3.32%) | 33.60% (σ=4.47%) | 29.02% (σ=3.99%) | + +#### Jetpack Compose + +| Measurement | Baseline | Minimum recording | Touch Only | Text & Input Only | Image Only | All | +|-------------|------------------|-------------------|------------------|-------------------|------------------|------------------| +| #1 | 30.1% | 29.6% | 36.7% | 34% | 40.4% | 42.2% | +| #2 | 32% | 29.8% | 30.9% | 27.5% | 44.9% | 31.7% | +| #3 | 30% | 38.2% | 31.8% | 35% | 43.7% | 42.1% | +| #4 | 37.1% | 31.4% | 34.8% | 39.4% | 40.1% | 45.4% | +| #5 | 26.3% | 42.2% | 33.6% | 33.4% | 44.9% | 43.8% | +| Average | 31.10% (σ=3.52%) | 34.24% (σ=5.07%) | 33.56% (σ=2.08%) | 33.86% (σ=3.81%) | 42.80% (σ=2.13%) | 41.04% (σ=4.82%) | + +### Memory Peak + +#### Android View + +| Measurement | Baseline | Minimum recording | Touch Only | Text & Input Only | Image Only | All | +|-------------|---------------------|---------------------|---------------------|---------------------|---------------------|-------------------| +| #1 | 265MB | 265MB | 291MB | 261MB | 317MB | 334MB | +| #2 | 248MB | 248MB | 319MB | 284MB | 287MB | 285MB | +| #3 | 250MB | 250MB | 156MB | 259MB | 311MB | 284MB | +| #4 | 221MB | 221MB | 318MB | 195MB | 341MB | 315MB | +| #5 | 275MB | 275MB | 301MB | 252MB | 274MB | 317MB | +| Average | 251.8MB (σ=18.32MB) | 251.8MB (σ=18.32MB) | 277.0MB (σ=61.41MB) | 250.2MB (σ=29.62MB) | 306.0MB (σ=23.48MB) | 307MB (σ=19.52MB) | + +#### Jetpack Compose + +| Measurement | Baseline | Minimum recording | Touch Only | Text & Input Only | Image Only | All | +|-------------|---------------------|---------------------|---------------------|---------------------|---------------------|---------------------| +| #1 | 265MB | 202MB | 291MB | 261MB | 317MB | 334MB | +| #2 | 248MB | 417MB | 319MB | 284MB | 287MB | 285MB | +| #3 | 250MB | 271MB | 156MB | 259MB | 311MB | 284MB | +| #4 | 221MB | 313MB | 318MB | 195MB | 341MB | 315MB | +| #5 | 275MB | 301MB | 301MB | 252MB | 274MB | 317MB | +| Average | 251.8MB (σ=18.32MB) | 300.8MB (σ=69.71MB) | 277.0MB (σ=61.41MB) | 250.2MB (σ=29.62MB) | 306.0MB (σ=23.48MB) | 307.0MB (σ=19.52MB) | + +### Janky Frames + +#### Android View + +| Measurement | Baseline | Minimum recording | Touch Only | Text & Input Only | Image Only | All | +|-------------|-----------------|-------------------|-----------------|-------------------|-----------------|-----------------| +| #1 | 139/3558 | 189/4139 | 298/5070 | 209/4381 | 161/3829 | 201/3079 | +| #2 | 156/5031 | 192/4963 | 225/4776 | 209/3524 | 209/3073 | 240/4235 | +| #3 | 69/2485 | 128/3173 | 259/4651 | 228/4281 | 240/3547 | 171/4008 | +| #4 | 114/4057 | 118/3569 | 122/4331 | 126/3423 | 194/4268 | 228/3886 | +| #5 | 132/4112 | 159/4369 | 201/4094 | 225/4430 | 167/3268 | 259/4491 | +| Average | 3.16% (σ=0.41%) | 3.88% (σ=0.42%) | 4.78% (σ=1.07%) | 4.96% (σ=0.74%) | 5.49% (σ=1.10%) | 5.62% (σ=0.74%) | + +#### Jetpack Compose + +| Measurement | Baseline | Minimum recording | Touch Only | Text & Input Only | Image Only | All | +|-------------|-----------------|-------------------|-----------------|-------------------|-----------------|-----------------| +| #1 | 157/5919 | 380/5191 | 368/5581 | 348/5227 | 393/5868 | 247/3644 | +| #2 | 120/4788 | 331/5115 | 427/4758 | 316/5393 | 410/5313 | 267/3514 | +| #3 | 131/4280 | 287/4572 | 345/5306 | 458/6743 | 336/5257 | 271/4392 | +| #4 | 198/6188 | 298/5057 | 354/5859 | 323/5054 | 323/5042 | 284/4678 | +| #5 | 159/4825 | 319/5051 | 346/5281 | 359/6693 | 411/5673 | 256/4096 | +| Average | 2.94% (σ=0.31%) | 6.46% (σ=0.47%) | 6.93% (σ=1.04%) | 6.21% (σ=0.53%) | 6.89% (σ=0.52%) | 6.57% (σ=0.57%) | \ No newline at end of file diff --git a/docs/trace_collection.md b/docs/trace_collection.md deleted file mode 100644 index 89adcd5ffb..0000000000 --- a/docs/trace_collection.md +++ /dev/null @@ -1,271 +0,0 @@ -# Android Trace Collection - -Send [traces][1] to Datadog from your Android applications with [Datadog's `dd-sdk-android` client-side tracing library][2] and leverage the following features: - -* Create custom [spans][3] for operations in your application. -* Add `context` and extra custom attributes to each span sent. -* Optimized network usage with automatic bulk posts. - -## Setup - -1. Add the Gradle dependency by declaring the library as a dependency in your `build.gradle` file: - - ```conf - repositories { - maven { url "https://dl.bintray.com/datadog/datadog-maven" } - } - - dependencies { - implementation "com.datadoghq:dd-sdk-android:x.x.x" - } - ``` - -2. Initialize the library with your application context and your [Datadog client token][4]. For security reasons, you must use a client token: you cannot use [Datadog API keys][5] to configure the `dd-sdk-android` library as they would be exposed client-side in the Android application APK byte code. For more information about setting up a client token, see the [client token documentation][4]: - - {{< tabs >}} - {{% tab "US" %}} - -```kotlin -class SampleApplication : Application() { - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "") - .build() - Datadog.initialize(this, config) - } -} -``` - - {{% /tab %}} - {{% tab "EU" %}} - -```kotlin -class SampleApplication : Application() { - override fun onCreate() { - super.onCreate() - - val config = DatadogConfig.Builder("", "", "") - .useEUEndpoints() - .build() - Datadog.initialize(this, config) - } -} -``` - - {{% /tab %}} - {{< /tabs >}} - -3. Configure and register the Android Tracer. You only need to do it once, usually in your application's `onCreate()` method: - - ```kotlin - val tracer = AndroidTracer.Builder().build() - GlobalTracer.registerIfAbsent(tracer) - ``` - -4. (Optional) - Set the partial flush threshold. You can optimize the workload of the SDK if you create a lot of spans in your application, or on the contrary very few of them. The library waits until the number of finished spans gets above the threshold to write them on disk. A value of `1` writes each span as soon as its finished. - - ```kotlin - val tracer = AndroidTracer.Builder() - .setPartialFlushThreshold(10) - .build() - ``` - -5. Start a custom span using the following method: - - ```kotlin - val tracer = GlobalTracer.get() - val span = tracer.buildSpan("").start() - // Do something ... - // ... - // Then when the span should be closed - span.finish() - - ``` -7. Using Scopes: - ```kotlin - val span = tracer.buildSpan("").start() - try { - val scope = tracer.activateSpan(span) - scope.use { - // Do something ... - // ... - // Start a new Scope - val childSpan = tracer.buildSpan("").start() - try { - tracer.activateSpan(childSpan).use { - // Do something ... - } - } - catch(e:Error){ - childSpan.error(e) - } - finally { - childSpan.finish() - } - } - } - catch(e:Error){ - span.error(e) - } - finally { - span.finish() - } - - ``` -8. Using scopes in Asynchronous calls: - ```kotlin - val span = tracer.buildSpan("").start() - try{ - val scope = tracer.activateSpan(span) - scope.use { - // Do something ... - doAsynWork { - // Step 2: reactivate the Span in the worker thread - val scopeContinuation = tracer.scopeManager().activate(span) - scopeContinuation.use { - // Do something ... - } - } - } - } - catch(e:Error){ - span.error(e) - } - finally{ - span.finish() - } - - ``` -9. (Optional) How to manually distribute traces between your environments, for example frontend - backend: - - * Step 1: Inject tracer context in the client request. - - ```kotlin - val tracer = GlobalTracer.get() - val span = tracer.buildSpan("").start() - val tracedRequestBuilder = Request.Builder() - tracer.inject( - span.context(), - Format.Builtin.TEXT_MAP_INJECT, - TextMapInject { key, value -> - tracedRequestBuilder.addHeader(key, value) - } - ) - val request = tracedRequestBuilder.build() - // Dispatch the request and finish the span after. - ``` - - * Step 2: Extract the client tracer context from headers in server code. - - ```kotlin - val extractedContext = GlobalTracer.get() - .extract( - Format.Builtin.TEXT_MAP_EXTRACT, - TextMapExtract { - request.headers() - .toMultimap() - .map { it.key to it.value.joinToString(";") } - .toMap() - .toMutableMap() - .iterator() - } - ) - val serverSpan = tracer.buildSpan("").asChildOf(extractedContext).start() - - ``` - -**Note**: For code bases using the OkHttp client, Datadog provides the implementation below. - -10. (Optional) - Provide additional tags alongside your span. - - ```kotlin - span.setTag("http.url", url) - ``` -11. (Optional) Attach an error information to a span: - - If you want to mark a span as having an error, you can do so by logging it using the official OpenTracing tags - - ```kotlin - span.log(mapOf(Fields.ERROR_OBJECT to throwable)) - ``` - ```kotlin - span.log(mapOf(Fields.MESSAGE to errorMessage)) - ``` - You can also use one of the following helper method in AndroidTracer - - ```kotlin - AndroidTracer.logThrowable(span, throwable) - ``` - ```kotlin - AndroidTracer.logErrorMessage(span, message) - ``` - -## Integrations - -In addition to manual tracing, the `dd-sdk-android` library provides the following integration. - -### OkHttp - -If you want to trace your OkHttp requests, you can add the provided [Interceptor][6] as follows: - -```kotlin -val okHttpClient = OkHttpClient.Builder() - .addInterceptor( - DatadogInterceptor( - listOf("example.com", "example.eu") - ) - ) - .build() -``` - -This creates a span around each request processed by the OkHttpClient (matching the provided hosts), with all the relevant information automatically filled (URL, method, status code, error), and propagates the tracing information to your backend to get a unified trace within Datadog. - -The interceptor tracks requests at the application level. You can also add a `TracingInterceptor` at the network level to get more details, for example when following redirections. - - ```kotlin -val tracedHosts = listOf("example.com", "example.eu") -val okHttpClient = OkHttpClient.Builder() - .addInterceptor(DatadogInterceptor(tracedHosts)) - .addNetworkInterceptor(TracingInterceptor(tracedHosts)) - .build() - ``` - -Because the way the OkHttp Request is executed (using a Thread pool), the request span won't be automatically linked with the span that triggered the request. You can manually provide a parent span in the `OkHttp Request.Builder` as follows: - -```kotlin -val request = Request.Builder() - .url(http://github.com/requestUrl) - .tag(Span::class.java, parentSpan) - .build() -``` - -or if you are using the extensions provided in the `dd-sdk-android-ktx` library: - -```kotlin -val request = Request.Builder() - .url(http://github.com/requestUrl) - .parentSpan(parentSpan) - .build() -``` - -**Note**: If you use multiple Interceptors, this one must be called first. - -## Batch collection - -All the spans are first stored on the local device in batches. Each batch follows the intake specification. They are sent as soon as network is available, and the battery is high enough to ensure the Datadog SDK does not impact the end user's experience. If the network is not available while your application is in the foreground, or if an upload of data fails, the batch is kept until it can be sent successfully. - -This means that even if users open your application while being offline, no data will be lost. - -The data on disk will automatically be discarded if it gets too old to ensure the SDK doesn't use too much disk space. - -## Further Reading - -{{< partial name="whats-next/whats-next.html" >}} - -[1]: https://docs.datadoghq.com/tracing/visualization/#trace -[2]: https://github.com/DataDog/dd-sdk-android -[3]: https://docs.datadoghq.com/tracing/visualization/#spans -[4]: https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens -[5]: https://docs.datadoghq.com/account_management/api-app-keys/#api-keys -[6]: https://square.github.io/okhttp/interceptors/ diff --git a/sample/java/.gitignore b/features/dd-sdk-android-logs/.gitignore similarity index 100% rename from sample/java/.gitignore rename to features/dd-sdk-android-logs/.gitignore diff --git a/features/dd-sdk-android-logs/README.md b/features/dd-sdk-android-logs/README.md new file mode 100644 index 0000000000..fe5370ffe0 --- /dev/null +++ b/features/dd-sdk-android-logs/README.md @@ -0,0 +1,5 @@ +# Datadog Logs SDK for Android + +See the dedicated [Datadog Android Log Collection documentation][1] to learn how to forward logs from your Android or Android TV application to Datadog. + +[1]: https://docs.datadoghq.com/logs/log_collection/android/?tab=kotlin \ No newline at end of file diff --git a/features/dd-sdk-android-logs/api/apiSurface b/features/dd-sdk-android-logs/api/apiSurface new file mode 100644 index 0000000000..145f63da6d --- /dev/null +++ b/features/dd-sdk-android-logs/api/apiSurface @@ -0,0 +1,138 @@ +class com.datadog.android.log.Logger + fun v(String, Throwable? = null, Map = emptyMap()) + fun d(String, Throwable? = null, Map = emptyMap()) + fun i(String, Throwable? = null, Map = emptyMap()) + fun w(String, Throwable? = null, Map = emptyMap()) + fun e(String, Throwable? = null, Map = emptyMap()) + fun wtf(String, Throwable? = null, Map = emptyMap()) + fun log(Int, String, Throwable? = null, Map = emptyMap()) + fun log(Int, String, String?, String?, String?, Map = emptyMap()) + class Builder + constructor(com.datadog.android.api.SdkCore = Datadog.getInstance()) + fun build(): Logger + fun setService(String): Builder + fun setRemoteLogThreshold(Int): Builder + fun setLogcatLogsEnabled(Boolean): Builder + fun setNetworkInfoEnabled(Boolean): Builder + fun setName(String): Builder + fun setBundleWithTraceEnabled(Boolean): Builder + fun setBundleWithRumEnabled(Boolean): Builder + fun setRemoteSampleRate(Float): Builder + fun addAttribute(String, Any?) + fun removeAttribute(String) + fun addTag(String, String) + fun addTag(String) + fun removeTag(String) + fun removeTagsWithKey(String) +object com.datadog.android.log.Logs + fun enable(LogsConfiguration, com.datadog.android.api.SdkCore = Datadog.getInstance()) + fun isEnabled(com.datadog.android.api.SdkCore = Datadog.getInstance()): Boolean + fun addAttribute(String, Any?, com.datadog.android.api.SdkCore = Datadog.getInstance()) + fun removeAttribute(String, com.datadog.android.api.SdkCore = Datadog.getInstance()) +data class com.datadog.android.log.LogsConfiguration + class Builder + fun useCustomEndpoint(String): Builder + fun setEventMapper(com.datadog.android.event.EventMapper): Builder + fun build(): LogsConfiguration +data class com.datadog.android.log.model.LogEvent + constructor(LogEventDevice, Os, Status, kotlin.String, kotlin.String, kotlin.String, Logger, Dd, Usr? = null, Account? = null, Network? = null, Error? = null, kotlin.String? = null, kotlin.String, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): LogEvent + fun fromJsonObject(com.google.gson.JsonObject): LogEvent + data class LogEventDevice + constructor(Type? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.List? = null, kotlin.String? = null, kotlin.Number? = null, kotlin.Boolean? = null, kotlin.Number? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): LogEventDevice + fun fromJsonObject(com.google.gson.JsonObject): LogEventDevice + data class Os + constructor(kotlin.String, kotlin.String, kotlin.String? = null, kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Os + fun fromJsonObject(com.google.gson.JsonObject): Os + data class Logger + constructor(kotlin.String, kotlin.String? = null, kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Logger + fun fromJsonObject(com.google.gson.JsonObject): Logger + data class Dd + constructor(DdDevice) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Dd + fun fromJsonObject(com.google.gson.JsonObject): Dd + data class Usr + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Usr + fun fromJsonObject(com.google.gson.JsonObject): Usr + data class Account + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Account + fun fromJsonObject(com.google.gson.JsonObject): Account + data class Network + constructor(Client) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Network + fun fromJsonObject(com.google.gson.JsonObject): Network + data class Error + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.List? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Error + fun fromJsonObject(com.google.gson.JsonObject): Error + data class DdDevice + constructor(kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): DdDevice + fun fromJsonObject(com.google.gson.JsonObject): DdDevice + data class Client + constructor(SimCarrier? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Client + fun fromJsonObject(com.google.gson.JsonObject): Client + data class Thread + constructor(kotlin.String, kotlin.Boolean, kotlin.String, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Thread + fun fromJsonObject(com.google.gson.JsonObject): Thread + data class SimCarrier + constructor(kotlin.String? = null, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): SimCarrier + fun fromJsonObject(com.google.gson.JsonObject): SimCarrier + enum Status + constructor(kotlin.String) + - CRITICAL + - ERROR + - WARN + - INFO + - DEBUG + - TRACE + - EMERGENCY + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Status + enum Type + constructor(kotlin.String) + - MOBILE + - DESKTOP + - TABLET + - TV + - GAMING_CONSOLE + - BOT + - OTHER + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Type diff --git a/features/dd-sdk-android-logs/api/dd-sdk-android-logs.api b/features/dd-sdk-android-logs/api/dd-sdk-android-logs.api new file mode 100644 index 0000000000..8cefa9ef22 --- /dev/null +++ b/features/dd-sdk-android-logs/api/dd-sdk-android-logs.api @@ -0,0 +1,502 @@ +public final class com/datadog/android/log/Logger { + public final fun addAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public final fun addTag (Ljava/lang/String;)V + public final fun addTag (Ljava/lang/String;Ljava/lang/String;)V + public final fun d (Ljava/lang/String;)V + public final fun d (Ljava/lang/String;Ljava/lang/Throwable;)V + public final fun d (Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;)V + public static synthetic fun d$default (Lcom/datadog/android/log/Logger;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;ILjava/lang/Object;)V + public final fun e (Ljava/lang/String;)V + public final fun e (Ljava/lang/String;Ljava/lang/Throwable;)V + public final fun e (Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;)V + public static synthetic fun e$default (Lcom/datadog/android/log/Logger;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;ILjava/lang/Object;)V + public final fun i (Ljava/lang/String;)V + public final fun i (Ljava/lang/String;Ljava/lang/Throwable;)V + public final fun i (Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;)V + public static synthetic fun i$default (Lcom/datadog/android/log/Logger;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;ILjava/lang/Object;)V + public final fun log (ILjava/lang/String;)V + public final fun log (ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun log (ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public final fun log (ILjava/lang/String;Ljava/lang/Throwable;)V + public final fun log (ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;)V + public static synthetic fun log$default (Lcom/datadog/android/log/Logger;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V + public static synthetic fun log$default (Lcom/datadog/android/log/Logger;ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;ILjava/lang/Object;)V + public final fun removeAttribute (Ljava/lang/String;)V + public final fun removeTag (Ljava/lang/String;)V + public final fun removeTagsWithKey (Ljava/lang/String;)V + public final fun v (Ljava/lang/String;)V + public final fun v (Ljava/lang/String;Ljava/lang/Throwable;)V + public final fun v (Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;)V + public static synthetic fun v$default (Lcom/datadog/android/log/Logger;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;ILjava/lang/Object;)V + public final fun w (Ljava/lang/String;)V + public final fun w (Ljava/lang/String;Ljava/lang/Throwable;)V + public final fun w (Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;)V + public static synthetic fun w$default (Lcom/datadog/android/log/Logger;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;ILjava/lang/Object;)V + public final fun wtf (Ljava/lang/String;)V + public final fun wtf (Ljava/lang/String;Ljava/lang/Throwable;)V + public final fun wtf (Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;)V + public static synthetic fun wtf$default (Lcom/datadog/android/log/Logger;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;ILjava/lang/Object;)V +} + +public final class com/datadog/android/log/Logger$Builder { + public fun ()V + public fun (Lcom/datadog/android/api/SdkCore;)V + public synthetic fun (Lcom/datadog/android/api/SdkCore;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun build ()Lcom/datadog/android/log/Logger; + public final fun setBundleWithRumEnabled (Z)Lcom/datadog/android/log/Logger$Builder; + public final fun setBundleWithTraceEnabled (Z)Lcom/datadog/android/log/Logger$Builder; + public final fun setLogcatLogsEnabled (Z)Lcom/datadog/android/log/Logger$Builder; + public final fun setName (Ljava/lang/String;)Lcom/datadog/android/log/Logger$Builder; + public final fun setNetworkInfoEnabled (Z)Lcom/datadog/android/log/Logger$Builder; + public final fun setRemoteLogThreshold (I)Lcom/datadog/android/log/Logger$Builder; + public final fun setRemoteSampleRate (F)Lcom/datadog/android/log/Logger$Builder; + public final fun setService (Ljava/lang/String;)Lcom/datadog/android/log/Logger$Builder; +} + +public final class com/datadog/android/log/Logs { + public static final field INSTANCE Lcom/datadog/android/log/Logs; + public static final fun addAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public static final fun addAttribute (Ljava/lang/String;Ljava/lang/Object;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun addAttribute$default (Ljava/lang/String;Ljava/lang/Object;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun enable (Lcom/datadog/android/log/LogsConfiguration;)V + public static final fun enable (Lcom/datadog/android/log/LogsConfiguration;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun enable$default (Lcom/datadog/android/log/LogsConfiguration;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V + public static final fun isEnabled ()Z + public static final fun isEnabled (Lcom/datadog/android/api/SdkCore;)Z + public static synthetic fun isEnabled$default (Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)Z + public static final fun removeAttribute (Ljava/lang/String;)V + public static final fun removeAttribute (Ljava/lang/String;Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun removeAttribute$default (Ljava/lang/String;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V +} + +public final class com/datadog/android/log/LogsConfiguration { + public final fun copy (Ljava/lang/String;Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/log/LogsConfiguration; + public static synthetic fun copy$default (Lcom/datadog/android/log/LogsConfiguration;Ljava/lang/String;Lcom/datadog/android/event/EventMapper;ILjava/lang/Object;)Lcom/datadog/android/log/LogsConfiguration; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/LogsConfiguration$Builder { + public fun ()V + public final fun build ()Lcom/datadog/android/log/LogsConfiguration; + public final fun setEventMapper (Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/log/LogsConfiguration$Builder; + public final fun useCustomEndpoint (Ljava/lang/String;)Lcom/datadog/android/log/LogsConfiguration$Builder; +} + +public final class com/datadog/android/log/model/LogEvent { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Companion; + public fun (Lcom/datadog/android/log/model/LogEvent$LogEventDevice;Lcom/datadog/android/log/model/LogEvent$Os;Lcom/datadog/android/log/model/LogEvent$Status;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/log/model/LogEvent$Logger;Lcom/datadog/android/log/model/LogEvent$Dd;Lcom/datadog/android/log/model/LogEvent$Usr;Lcom/datadog/android/log/model/LogEvent$Account;Lcom/datadog/android/log/model/LogEvent$Network;Lcom/datadog/android/log/model/LogEvent$Error;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Lcom/datadog/android/log/model/LogEvent$LogEventDevice;Lcom/datadog/android/log/model/LogEvent$Os;Lcom/datadog/android/log/model/LogEvent$Status;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/log/model/LogEvent$Logger;Lcom/datadog/android/log/model/LogEvent$Dd;Lcom/datadog/android/log/model/LogEvent$Usr;Lcom/datadog/android/log/model/LogEvent$Account;Lcom/datadog/android/log/model/LogEvent$Network;Lcom/datadog/android/log/model/LogEvent$Error;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/datadog/android/log/model/LogEvent$LogEventDevice; + public final fun component10 ()Lcom/datadog/android/log/model/LogEvent$Account; + public final fun component11 ()Lcom/datadog/android/log/model/LogEvent$Network; + public final fun component12 ()Lcom/datadog/android/log/model/LogEvent$Error; + public final fun component13 ()Ljava/lang/String; + public final fun component14 ()Ljava/lang/String; + public final fun component15 ()Ljava/util/Map; + public final fun component2 ()Lcom/datadog/android/log/model/LogEvent$Os; + public final fun component3 ()Lcom/datadog/android/log/model/LogEvent$Status; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Lcom/datadog/android/log/model/LogEvent$Logger; + public final fun component8 ()Lcom/datadog/android/log/model/LogEvent$Dd; + public final fun component9 ()Lcom/datadog/android/log/model/LogEvent$Usr; + public final fun copy (Lcom/datadog/android/log/model/LogEvent$LogEventDevice;Lcom/datadog/android/log/model/LogEvent$Os;Lcom/datadog/android/log/model/LogEvent$Status;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/log/model/LogEvent$Logger;Lcom/datadog/android/log/model/LogEvent$Dd;Lcom/datadog/android/log/model/LogEvent$Usr;Lcom/datadog/android/log/model/LogEvent$Account;Lcom/datadog/android/log/model/LogEvent$Network;Lcom/datadog/android/log/model/LogEvent$Error;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/log/model/LogEvent; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent;Lcom/datadog/android/log/model/LogEvent$LogEventDevice;Lcom/datadog/android/log/model/LogEvent$Os;Lcom/datadog/android/log/model/LogEvent$Status;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/log/model/LogEvent$Logger;Lcom/datadog/android/log/model/LogEvent$Dd;Lcom/datadog/android/log/model/LogEvent$Usr;Lcom/datadog/android/log/model/LogEvent$Account;Lcom/datadog/android/log/model/LogEvent$Network;Lcom/datadog/android/log/model/LogEvent$Error;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent; + public final fun getAccount ()Lcom/datadog/android/log/model/LogEvent$Account; + public final fun getAdditionalProperties ()Ljava/util/Map; + public final fun getBuildId ()Ljava/lang/String; + public final fun getDate ()Ljava/lang/String; + public final fun getDd ()Lcom/datadog/android/log/model/LogEvent$Dd; + public final fun getDdtags ()Ljava/lang/String; + public final fun getDevice ()Lcom/datadog/android/log/model/LogEvent$LogEventDevice; + public final fun getError ()Lcom/datadog/android/log/model/LogEvent$Error; + public final fun getLogger ()Lcom/datadog/android/log/model/LogEvent$Logger; + public final fun getMessage ()Ljava/lang/String; + public final fun getNetwork ()Lcom/datadog/android/log/model/LogEvent$Network; + public final fun getOs ()Lcom/datadog/android/log/model/LogEvent$Os; + public final fun getService ()Ljava/lang/String; + public final fun getStatus ()Lcom/datadog/android/log/model/LogEvent$Status; + public final fun getUsr ()Lcom/datadog/android/log/model/LogEvent$Usr; + public fun hashCode ()I + public final fun setDdtags (Ljava/lang/String;)V + public final fun setMessage (Ljava/lang/String;)V + public final fun setStatus (Lcom/datadog/android/log/model/LogEvent$Status;)V + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Account { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Account$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/log/model/LogEvent$Account; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Account;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Account; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Account; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Account; + public final fun getAdditionalProperties ()Ljava/util/Map; + public final fun getId ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Account$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Account; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Account; +} + +public final class com/datadog/android/log/model/LogEvent$Client { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Client$Companion; + public fun (Lcom/datadog/android/log/model/LogEvent$SimCarrier;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Lcom/datadog/android/log/model/LogEvent$SimCarrier;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/datadog/android/log/model/LogEvent$SimCarrier; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Lcom/datadog/android/log/model/LogEvent$SimCarrier;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Client; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Client;Lcom/datadog/android/log/model/LogEvent$SimCarrier;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Client; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Client; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Client; + public final fun getConnectivity ()Ljava/lang/String; + public final fun getDownlinkKbps ()Ljava/lang/String; + public final fun getSignalStrength ()Ljava/lang/String; + public final fun getSimCarrier ()Lcom/datadog/android/log/model/LogEvent$SimCarrier; + public final fun getUplinkKbps ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Client$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Client; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Client; +} + +public final class com/datadog/android/log/model/LogEvent$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent; +} + +public final class com/datadog/android/log/model/LogEvent$Dd { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Dd$Companion; + public fun (Lcom/datadog/android/log/model/LogEvent$DdDevice;)V + public final fun component1 ()Lcom/datadog/android/log/model/LogEvent$DdDevice; + public final fun copy (Lcom/datadog/android/log/model/LogEvent$DdDevice;)Lcom/datadog/android/log/model/LogEvent$Dd; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Dd;Lcom/datadog/android/log/model/LogEvent$DdDevice;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Dd; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Dd; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Dd; + public final fun getDevice ()Lcom/datadog/android/log/model/LogEvent$DdDevice; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Dd$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Dd; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Dd; +} + +public final class com/datadog/android/log/model/LogEvent$DdDevice { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$DdDevice$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$DdDevice; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$DdDevice;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$DdDevice; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$DdDevice; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$DdDevice; + public final fun getArchitecture ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$DdDevice$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$DdDevice; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$DdDevice; +} + +public final class com/datadog/android/log/model/LogEvent$Error { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Error$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/datadog/android/log/model/LogEvent$Error; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Error;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Error; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Error; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Error; + public final fun getFingerprint ()Ljava/lang/String; + public final fun getKind ()Ljava/lang/String; + public final fun getMessage ()Ljava/lang/String; + public final fun getSourceType ()Ljava/lang/String; + public final fun getStack ()Ljava/lang/String; + public final fun getThreads ()Ljava/util/List; + public fun hashCode ()I + public final fun setFingerprint (Ljava/lang/String;)V + public final fun setKind (Ljava/lang/String;)V + public final fun setMessage (Ljava/lang/String;)V + public final fun setSourceType (Ljava/lang/String;)V + public final fun setStack (Ljava/lang/String;)V + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Error$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Error; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Error; +} + +public final class com/datadog/android/log/model/LogEvent$LogEventDevice { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$LogEventDevice$Companion; + public fun ()V + public fun (Lcom/datadog/android/log/model/LogEvent$Type;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Number;)V + public synthetic fun (Lcom/datadog/android/log/model/LogEvent$Type;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Number;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/datadog/android/log/model/LogEvent$Type; + public final fun component10 ()Ljava/lang/Boolean; + public final fun component11 ()Ljava/lang/Number; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Ljava/lang/Number; + public final fun copy (Lcom/datadog/android/log/model/LogEvent$Type;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Number;)Lcom/datadog/android/log/model/LogEvent$LogEventDevice; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$LogEventDevice;Lcom/datadog/android/log/model/LogEvent$Type;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$LogEventDevice; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$LogEventDevice; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$LogEventDevice; + public final fun getArchitecture ()Ljava/lang/String; + public final fun getBatteryLevel ()Ljava/lang/Number; + public final fun getBrand ()Ljava/lang/String; + public final fun getBrightnessLevel ()Ljava/lang/Number; + public final fun getLocale ()Ljava/lang/String; + public final fun getLocales ()Ljava/util/List; + public final fun getModel ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getPowerSavingMode ()Ljava/lang/Boolean; + public final fun getTimeZone ()Ljava/lang/String; + public final fun getType ()Lcom/datadog/android/log/model/LogEvent$Type; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$LogEventDevice$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$LogEventDevice; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$LogEventDevice; +} + +public final class com/datadog/android/log/model/LogEvent$Logger { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Logger$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Logger; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Logger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Logger; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Logger; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Logger; + public final fun getName ()Ljava/lang/String; + public final fun getThreadName ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public final fun setName (Ljava/lang/String;)V + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Logger$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Logger; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Logger; +} + +public final class com/datadog/android/log/model/LogEvent$Network { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Network$Companion; + public fun (Lcom/datadog/android/log/model/LogEvent$Client;)V + public final fun component1 ()Lcom/datadog/android/log/model/LogEvent$Client; + public final fun copy (Lcom/datadog/android/log/model/LogEvent$Client;)Lcom/datadog/android/log/model/LogEvent$Network; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Network;Lcom/datadog/android/log/model/LogEvent$Client;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Network; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Network; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Network; + public final fun getClient ()Lcom/datadog/android/log/model/LogEvent$Client; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Network$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Network; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Network; +} + +public final class com/datadog/android/log/model/LogEvent$Os { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Os$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Os; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Os;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Os; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Os; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Os; + public final fun getBuild ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public final fun getVersionMajor ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Os$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Os; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Os; +} + +public final class com/datadog/android/log/model/LogEvent$SimCarrier { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$SimCarrier$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$SimCarrier; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$SimCarrier;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$SimCarrier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$SimCarrier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$SimCarrier; + public final fun getId ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$SimCarrier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$SimCarrier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$SimCarrier; +} + +public final class com/datadog/android/log/model/LogEvent$Status : java/lang/Enum { + public static final field CRITICAL Lcom/datadog/android/log/model/LogEvent$Status; + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Status$Companion; + public static final field DEBUG Lcom/datadog/android/log/model/LogEvent$Status; + public static final field EMERGENCY Lcom/datadog/android/log/model/LogEvent$Status; + public static final field ERROR Lcom/datadog/android/log/model/LogEvent$Status; + public static final field INFO Lcom/datadog/android/log/model/LogEvent$Status; + public static final field TRACE Lcom/datadog/android/log/model/LogEvent$Status; + public static final field WARN Lcom/datadog/android/log/model/LogEvent$Status; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Status; + public final fun toJson ()Lcom/google/gson/JsonElement; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Status; + public static fun values ()[Lcom/datadog/android/log/model/LogEvent$Status; +} + +public final class com/datadog/android/log/model/LogEvent$Status$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Status; +} + +public final class com/datadog/android/log/model/LogEvent$Thread { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Thread$Companion; + public fun (Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Z + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Thread; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Thread;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Thread; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Thread; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Thread; + public final fun getCrashed ()Z + public final fun getName ()Ljava/lang/String; + public final fun getStack ()Ljava/lang/String; + public final fun getState ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Thread$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Thread; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Thread; +} + +public final class com/datadog/android/log/model/LogEvent$Type : java/lang/Enum { + public static final field BOT Lcom/datadog/android/log/model/LogEvent$Type; + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Type$Companion; + public static final field DESKTOP Lcom/datadog/android/log/model/LogEvent$Type; + public static final field GAMING_CONSOLE Lcom/datadog/android/log/model/LogEvent$Type; + public static final field MOBILE Lcom/datadog/android/log/model/LogEvent$Type; + public static final field OTHER Lcom/datadog/android/log/model/LogEvent$Type; + public static final field TABLET Lcom/datadog/android/log/model/LogEvent$Type; + public static final field TV Lcom/datadog/android/log/model/LogEvent$Type; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Type; + public final fun toJson ()Lcom/google/gson/JsonElement; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Type; + public static fun values ()[Lcom/datadog/android/log/model/LogEvent$Type; +} + +public final class com/datadog/android/log/model/LogEvent$Type$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Type; +} + +public final class com/datadog/android/log/model/LogEvent$Usr { + public static final field Companion Lcom/datadog/android/log/model/LogEvent$Usr$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/log/model/LogEvent$Usr; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Usr;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Usr; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Usr; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Usr; + public final fun getAdditionalProperties ()Ljava/util/Map; + public final fun getEmail ()Ljava/lang/String; + public final fun getId ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/log/model/LogEvent$Usr$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Usr; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Usr; +} + diff --git a/features/dd-sdk-android-logs/build.gradle.kts b/features/dd-sdk-android-logs/build.gradle.kts new file mode 100644 index 0000000000..be802a3fff --- /dev/null +++ b/features/dd-sdk-android-logs/build.gradle.kts @@ -0,0 +1,91 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +@file:Suppress("StringLiteralDuplication") + +import com.datadog.gradle.config.androidLibraryConfig +import com.datadog.gradle.config.dependencyUpdateConfig +import com.datadog.gradle.config.detektCustomConfig +import com.datadog.gradle.config.javadocConfig +import com.datadog.gradle.config.junitConfig +import com.datadog.gradle.config.kotlinConfig +import com.datadog.gradle.config.publishingConfig +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import java.nio.file.Paths + +plugins { + // Build + id("com.android.library") + kotlin("android") + id("com.google.devtools.ksp") + + // Publish + `maven-publish` + signing + id("org.jetbrains.dokka-javadoc") + + // Analysis tools + id("com.github.ben-manes.versions") + + // Tests + id("de.mobilej.unmock") + id("org.jetbrains.kotlinx.kover") + + // Internal Generation + id("com.datadoghq.dependency-license") + id("apiSurface") + id("transitiveDependencies") + id("verificationXml") + id("binary-compatibility-validator") +} + +android { + defaultConfig { + consumerProguardFiles(Paths.get(rootDir.path, "consumer-rules.pro").toString()) + } + + namespace = "com.datadog.android.log" +} + +dependencies { + api(project(":dd-sdk-android-core")) + implementation(project(":dd-sdk-android-internal")) + implementation(libs.kotlin) + implementation(libs.gson) + implementation(libs.androidXAnnotation) + + // Generate NoOp implementations + ksp(project(":tools:noopfactory")) + + testImplementation(project(":tools:unit")) { + attributes { + attribute( + com.android.build.api.attributes.ProductFlavorAttr.of("platform"), + objects.named("jvm") + ) + } + } + testImplementation(testFixtures(project(":dd-sdk-android-core"))) + testImplementation(libs.bundles.jUnit5) + testImplementation(libs.bundles.testTools) + testImplementation(libs.okHttp) + unmock(libs.robolectric) +} + +unMock { + keepStartingWith("org.json") +} + +apply(from = "generate_log_models.gradle.kts") +kotlinConfig(jvmBytecodeTarget = JvmTarget.JVM_11) +androidLibraryConfig() +junitConfig() +javadocConfig() +dependencyUpdateConfig() +publishingConfig( + "The Logs feature to use with the Datadog monitoring " + + "library for Android applications." +) +detektCustomConfig() diff --git a/features/dd-sdk-android-logs/clone_common_schema.gradle.kts b/features/dd-sdk-android-logs/clone_common_schema.gradle.kts new file mode 100644 index 0000000000..0279d19459 --- /dev/null +++ b/features/dd-sdk-android-logs/clone_common_schema.gradle.kts @@ -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 2016-Present Datadog, Inc. + */ +import com.datadog.gradle.plugin.gitclone.GitCloneDependenciesTask + +tasks.register("cloneCommonSchema") { + extension.apply { + clone( + "https://github.com/DataDog/rum-events-format.git", + "schemas/rum", + destinationFolder = "src/main/json/log", + excludedPrefixes = listOf( + "_action-child-schema.json", + "_perf-metric-schema.json", + "_profiling-internal-context-schema.json", + "_rect-schema.json", + "_view-accessibility-schema.json", + "_view-container-schema.json", + "_view-performance-schema.json", + "action-schema.json", + "error-schema.json", + "long_task-schema.json", + "resource-schema.json", + "view-schema.json", + "vital-schema.json" + ), + ref = "master" + ) + } +} diff --git a/features/dd-sdk-android-logs/generate_log_models.gradle.kts b/features/dd-sdk-android-logs/generate_log_models.gradle.kts new file mode 100644 index 0000000000..30f4b372f3 --- /dev/null +++ b/features/dd-sdk-android-logs/generate_log_models.gradle.kts @@ -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 2016-Present Datadog, Inc. + */ + +import com.datadog.gradle.plugin.apisurface.ApiSurfacePlugin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +val generateLogModelsTaskName = "generateLogModelsFromJson" + +tasks.register( + generateLogModelsTaskName, + com.datadog.gradle.plugin.jsonschema.GenerateJsonSchemaTask::class.java +) { + inputDirPath = "src/main/json/log" + ignoredFiles = arrayOf( + "_common-schema.json" + ) + targetPackageName = "com.datadog.android.log.model" +} + +afterEvaluate { + tasks.findByName(ApiSurfacePlugin.TASK_GEN_KOTLIN_API_SURFACE) + ?.dependsOn(generateLogModelsTaskName) + tasks.withType(KotlinCompile::class.java).configureEach { + dependsOn(generateLogModelsTaskName) + } +} diff --git a/features/dd-sdk-android-logs/src/main/json/log/log-schema.json b/features/dd-sdk-android-logs/src/main/json/log/log-schema.json new file mode 100644 index 0000000000..01dbbfacf8 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/json/log/log-schema.json @@ -0,0 +1,277 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "log-schema.json", + "title": "LogEvent", + "type": "object", + "description": "Structure holding information about a Log", + "properties": { + "device": { + "$ref": "../../../../../dd-sdk-android-rum/src/main/json/rum/_common-schema.json#/properties/device" + }, + "os": { + "$ref": "../../../../../dd-sdk-android-rum/src/main/json/rum/_common-schema.json#/properties/os" + }, + "status": { + "type": "string", + "description": "The severity of this log", + "enum": [ + "critical", + "error", + "warn", + "info", + "debug", + "trace", + "emergency" + ], + "readOnly": false + }, + "service": { + "type": "string", + "description": "The service name", + "readOnly": true + }, + "message": { + "type": "string", + "description": "The log message", + "readOnly": false + }, + "date": { + "type": "string", + "description": "The date when the log is fired as an ISO-8601 String", + "readOnly": true + }, + "logger": { + "type": "object", + "description": "Information about the logger that produced this log.", + "properties": { + "name": { + "type": "string", + "description": "The name of the logger", + "readOnly": false + }, + "thread_name": { + "type": "string", + "description": "The thread name on which the log event was created", + "readOnly": true + }, + "version": { + "type": "string", + "description": "The SDK version name", + "readOnly": true + } + }, + "required": [ + "name", + "version" + ], + "readOnly": true + }, + "_dd": { + "type": "object", + "description": "Datadog internal information", + "properties": { + "device": { + "type": "object", + "description": "Information about the device that produced this log.", + "properties": { + "architecture": { + "type": "string", + "description": "The CPU architecture of the device", + "readOnly": true + } + }, + "required": [ + "architecture" + ], + "readOnly": true + } + }, + "required": [ + "device" + ], + "readOnly": true + }, + "usr": { + "type": "object", + "description": "User properties", + "properties": { + "id": { + "type": "string", + "description": "Identifier of the user", + "readOnly": true + }, + "name": { + "type": "string", + "description": "Name of the user", + "readOnly": true + }, + "email": { + "type": "string", + "description": "Email of the user", + "readOnly": true + } + }, + "additionalProperties": { + "type": "object" + }, + "readOnly": true + }, + "account": { + "type": "object", + "description": "Account properties", + "properties": { + "id": { + "type": "string", + "description": "Identifier of the account", + "readOnly": true + }, + "name": { + "type": "string", + "description": "Name of the account", + "readOnly": true + } + }, + "additionalProperties": { + "type": "object" + }, + "readOnly": true + }, + "network": { + "type": "object", + "description": "The network information in the moment the log was created", + "properties": { + "client": { + "type": "object", + "properties": { + "sim_carrier": { + "type": "object", + "properties": { + "id": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "readOnly": true + } + } + }, + "signal_strength": { + "type": "string" + }, + "downlink_kbps": { + "type": "string", + "readOnly": true + }, + "uplink_kbps": { + "type": "string", + "readOnly": true + }, + "connectivity": { + "type": "string", + "description": "The active network", + "readOnly": true + } + }, + "readOnly": true, + "required": [ + "connectivity" + ] + } + }, + "readOnly": true, + "required": [ + "client" + ] + }, + "error": { + "type": "object", + "description": "The additional error information in case this log is marked as an error", + "properties": { + "kind": { + "type": "string", + "description": "The kind of this error. It is resolved from the throwable class name", + "readOnly": false + }, + "message": { + "type": "string", + "description": "The error message", + "readOnly": false + }, + "stack": { + "type": "string", + "description": "The error stack trace", + "readOnly": false + }, + "source_type": { + "type": "string", + "description": "The source_type of the error (e.g. 'android', 'flutter', 'react-native')", + "readOnly": false + }, + "fingerprint": { + "type": "string", + "description": "A custom fingerprint for this error", + "readOnly": false + }, + "threads": { + "type": "array", + "description": "Description of each thread in the process when error happened.", + "items": { + "type": "object", + "description": "Description of the thread in the process when error happened.", + "required": ["name", "crashed", "stack"], + "properties": { + "name": { + "type": "string", + "description": "Name of the thread (e.g. 'Thread 0').", + "readOnly": true + }, + "crashed": { + "type": "boolean", + "description": "Tells if the thread crashed.", + "readOnly": true + }, + "stack": { + "type": "string", + "description": "Unsymbolicated stack trace of the given thread.", + "readOnly": true + }, + "state": { + "type": "string", + "description": "Platform-specific state of the thread when its state was captured (CPU registers dump for iOS, thread state enum for Android, etc.).", + "readOnly": true + } + } + } + } + }, + "readOnly": true + }, + "build_id": { + "type": "string", + "description": "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.", + "readOnly": true + }, + "ddtags": { + "type": "string", + "description": "The list of tags joined into a String and divided by ',' ", + "readOnly": false + } + }, + "required": [ + "message", + "status", + "date", + "service", + "logger", + "_dd", + "ddtags", + "device", + "os" + ], + "additionalProperties": { + "type": "object", + "description": "additional log attributes" + } +} + diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logger.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logger.kt new file mode 100644 index 0000000000..f3beb01537 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logger.kt @@ -0,0 +1,523 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log + +import androidx.annotation.FloatRange +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.sampling.RateBasedSampler +import com.datadog.android.internal.utils.NULL_MAP_VALUE +import com.datadog.android.log.internal.LogsFeature +import com.datadog.android.log.internal.domain.DatadogLogGenerator +import com.datadog.android.log.internal.logger.CombinedLogHandler +import com.datadog.android.log.internal.logger.DatadogLogHandler +import com.datadog.android.log.internal.logger.LogHandler +import com.datadog.android.log.internal.logger.LogcatLogHandler +import com.datadog.android.log.internal.logger.NoOpLogHandler +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet +import android.util.Log as AndroidLog + +/** + * A class enabling Datadog logging features. + * + * It allows you to create a specific context (automatic information, custom attributes, tags) that + * will be embedded in all logs sent through this logger. + * + * You can have multiple loggers configured in your application, each with their own settings. + */ +@Suppress("TooManyFunctions", "MethodOverloading") +class Logger +internal constructor(internal var handler: LogHandler) { + + private val attributes = ConcurrentHashMap() + internal val tags = CopyOnWriteArraySet() + + // region Log + + /** + * Sends a VERBOSE log message. + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @Suppress("FunctionMinLength") + @JvmOverloads + fun v( + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap() + ) { + internalLog(AndroidLog.VERBOSE, message, throwable, attributes) + } + + /** + * Sends a Debug log message. + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @Suppress("FunctionMinLength") + @JvmOverloads + fun d( + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap() + ) { + internalLog(AndroidLog.DEBUG, message, throwable, attributes) + } + + /** + * Sends an Info log message. + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @Suppress("FunctionMinLength") + @JvmOverloads + fun i( + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap() + ) { + internalLog(AndroidLog.INFO, message, throwable, attributes) + } + + /** + * Sends a Warning log message. + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @Suppress("FunctionMinLength") + @JvmOverloads + fun w( + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap() + ) { + internalLog(AndroidLog.WARN, message, throwable, attributes) + } + + /** + * Sends an Error log message. + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @Suppress("FunctionMinLength") + @JvmOverloads + fun e( + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap() + ) { + internalLog(AndroidLog.ERROR, message, throwable, attributes) + } + + /** + * Sends an Assert log message. + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @Suppress("FunctionMinLength") + @JvmOverloads + fun wtf( + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap() + ) { + internalLog(AndroidLog.ASSERT, message, throwable, attributes) + } + + /** + * Sends a log message. + * + * @param priority the priority level (must be one of the Android Log.* constants) + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @JvmOverloads + fun log( + priority: Int, + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap() + ) { + internalLog(priority, message, throwable, attributes) + } + + /** + * Sends a log message with strings for error information. + * + * 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`. + * + * @param priority the priority level (must be one of the Android Log.* constants) + * @param message the message to be logged + * @param errorKind the kind of error to be logged with the message + * @param errorMessage the message from the error to be logged with this message + * @param errorStacktrace the stack trace from the error to be logged with this message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exist in this logger, it will be overridden (just for this message) + */ + @JvmOverloads + @Suppress("LongParameterList") + fun log( + priority: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStacktrace: String?, + attributes: Map = emptyMap() + ) { + internalLog(priority, message, errorKind, errorMessage, errorStacktrace, attributes) + } + + // endregion + + // region Builder + + /** + * A Builder class for a [Logger]. + * + * @param sdkCore SDK instance to bind to. If not provided, default instance will be used. + */ + class Builder + @JvmOverloads + constructor(sdkCore: SdkCore = Datadog.getInstance()) { + + private val sdkCore: FeatureSdkCore = sdkCore as FeatureSdkCore + + private var serviceName: String? = null + private var loggerName: String? = null + private var logcatLogsEnabled: Boolean = false + private var networkInfoEnabled: Boolean = false + private var bundleWithTraceEnabled: Boolean = true + private var bundleWithRumEnabled: Boolean = true + private var sampleRate: Float = DEFAULT_SAMPLE_RATE + private var minDatadogLogsPriority: Int = -1 + + /** + * Builds a [Logger] based on the current state of this Builder. + */ + fun build(): Logger { + val logsFeature = sdkCore + .getFeature(Feature.LOGS_FEATURE_NAME) + ?.unwrap() + val datadogLogsEnabled = sampleRate > 0 + val handler = when { + datadogLogsEnabled && logcatLogsEnabled -> { + CombinedLogHandler( + buildDatadogHandler(sdkCore, logsFeature), + buildLogcatHandler(sdkCore) + ) + } + + datadogLogsEnabled -> buildDatadogHandler(sdkCore, logsFeature) + logcatLogsEnabled -> buildLogcatHandler(sdkCore) + else -> NoOpLogHandler() + } + + return Logger(handler) + } + + /** + * Sets the service name that will appear in your logs. + * @param service the service name (default = application package name) + */ + fun setService(service: String): Builder { + this.serviceName = service + return this + } + + /** + * Sets a minimum threshold (priority) for the log to be sent to the Datadog servers. If log priority + * is below this one, then it won't be sent. Default value is -1 (allow all). + * @param minLogThreshold Minimum log threshold to be sent to the Datadog servers. + */ + fun setRemoteLogThreshold(minLogThreshold: Int): Builder { + minDatadogLogsPriority = minLogThreshold + return this + } + + /** + * Enables your logs to be duplicated in LogCat. + * @param enabled false by default + */ + fun setLogcatLogsEnabled(enabled: Boolean): Builder { + logcatLogsEnabled = enabled + return this + } + + /** + * Enables network information to be automatically added in your logs. + * @param enabled false by default + */ + fun setNetworkInfoEnabled(enabled: Boolean): Builder { + networkInfoEnabled = enabled + return this + } + + /** + * Sets the logger name that will appear in your logs when a throwable is attached. + * @param name the logger custom name (default = application package name) + */ + fun setName(name: String): Builder { + loggerName = name + return this + } + + /** + * Enables the logs bundling with the current active trace. If this feature is enabled all + * the logs from this moment on will be bundled with the current trace and you will be able + * to see all the logs sent during a specific trace. + * @param enabled true by default + */ + fun setBundleWithTraceEnabled(enabled: Boolean): Builder { + bundleWithTraceEnabled = enabled + return this + } + + /** + * Enables the logs bundling with the current active View. If this feature is enabled all + * the logs from this moment on will be bundled with the current view information and you + * will be able to see all the logs sent during a specific view in the Rum Explorer. + * @param enabled true by default + */ + fun setBundleWithRumEnabled(enabled: Boolean): Builder { + bundleWithRumEnabled = enabled + return this + } + + /** + * Sets the sample rate for this Logger. + * @param sampleRate the sample rate, in percent. + * A value of `30` means we'll send 30% of the logs. If value is `0`, no logs will be sent + * to Datadog. + * Default is 100.0 (ie: all logs are sent). + */ + fun setRemoteSampleRate(@FloatRange(from = 0.0, to = 100.0) sampleRate: Float): Builder { + this.sampleRate = sampleRate + return this + } + + // region Internal + + private fun buildLogcatHandler(sdkCore: SdkCore?): LogHandler { + return LogcatLogHandler( + serviceName = serviceName ?: sdkCore?.service ?: "unknown", + useClassnameAsTag = true + ) + } + + private fun buildDatadogHandler( + sdkCore: FeatureSdkCore, + logsFeature: LogsFeature? + ): LogHandler { + if (logsFeature == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { SDK_NOT_INITIALIZED_WARNING_MESSAGE } + ) + return NoOpLogHandler() + } + + return DatadogLogHandler( + sdkCore = sdkCore, + loggerName = loggerName ?: logsFeature.packageName, + logGenerator = DatadogLogGenerator( + serviceName ?: sdkCore.service + ), + writer = logsFeature.dataWriter, + minLogPriority = minDatadogLogsPriority, + bundleWithTraces = bundleWithTraceEnabled, + bundleWithRum = bundleWithRumEnabled, + sampler = RateBasedSampler(sampleRate), + attachNetworkInfo = networkInfoEnabled + ) + } + + // endregion + } + + // endregion + + // region Context Information (attributes, tags) + /** + * Add a custom attribute to all future logs sent by this logger. + * + * Values can be nested up to 10 levels deep. Keys + * using more than 10 levels will be sanitized by SDK. + * + * @param key the key for this attribute + * @param value the attribute value + */ + fun addAttribute(key: String, value: Any?) { + if (value == null) { + attributes[key] = NULL_MAP_VALUE + } else { + attributes[key] = value + } + } + + /** + * Remove a custom attribute from all future logs sent by this logger. + * Previous logs won't lose the attribute value associated with this key if they were created + * prior to this call. + * @param key the key of the attribute to remove + */ + fun removeAttribute(key: String) { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + attributes.remove(key) + } + + /** + * Add a tag to all future logs sent by this logger. + * The tag will take the form "key:value". + * + * Tags must start with a letter and after that may contain the following characters: + * Alphanumerics, Underscores, Minuses, Colons, Periods, Slashes. Other special characters + * are converted to underscores. + * Tags must be lowercase, and can be at most 200 characters. If the tag you provide is + * longer, only the first 200 characters will be used. + * + * @param key the key for this tag + * @param value the (non null) value of this tag + * @see [documentation](https://docs.datadoghq.com/tagging/#defining-tags) + */ + fun addTag(key: String, value: String) { + addTagInternal("$key:$value") + } + + /** + * Add a tag to all future logs sent by this logger. + * + * Tags must start with a letter and after that may contain the following characters: + * Alphanumerics, Underscores, Minuses, Colons, Periods, Slashes. Other special characters + * are converted to underscores. + * Tags must be lowercase, and can be at most 200 characters. If the tag you provide is + * longer, only the first 200 characters will be used. + * + * @param tag the (non null) tag + * @see [documentation](https://docs.datadoghq.com/tagging/#defining-tags) + */ + fun addTag(tag: String) { + addTagInternal(tag) + } + + /** + * Remove a tag from all future logs sent by this logger. + * Previous logs won't lose the this tag if they were created prior to this call. + * @param tag the tag to remove + */ + fun removeTag(tag: String) { + removeTagInternal(tag) + } + + /** + * Remove all tags with the given key from all future logs sent by this logger. + * Previous logs won't lose the this tag if they were created prior to this call. + * @param key the key of the tags to remove + */ + fun removeTagsWithKey(key: String) { + val prefix = "$key:" + safelyRemoveTagsWithKey { + it.startsWith(prefix) + } + } + + // endregion + + // region Internal + + internal fun internalLog( + level: Int, + message: String, + throwable: Throwable?, + localAttributes: Map, + timestamp: Long? = null + ) { + val combinedAttributes = mutableMapOf() + combinedAttributes.putAll(attributes) + combinedAttributes.putAll(localAttributes) + // need to make a copy, because the content will be access on another thread and it + // can change by then + val tagsSnapshot = HashSet(tags) + handler.handleLog(level, message, throwable, combinedAttributes, tagsSnapshot, timestamp) + } + + @Suppress("LongParameterList") + private fun internalLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStacktrace: String?, + localAttributes: Map, + timestamp: Long? = null + ) { + val combinedAttributes = mutableMapOf() + combinedAttributes.putAll(attributes) + combinedAttributes.putAll(localAttributes) + // need to make a copy, because the content will be access on another thread and it + // can change by then + val tagsSnapshot = HashSet(tags) + handler.handleLog( + level, + message, + errorKind, + errorMessage, + errorStacktrace, + combinedAttributes, + tagsSnapshot, + timestamp + ) + } + + private fun addTagInternal(tag: String) { + tags.add(tag) + } + + private fun removeTagInternal(tag: String) { + tags.remove(tag) + } + + private fun safelyRemoveTagsWithKey(keyFilter: (String) -> Boolean) { + // we first gather all the objects we want to remove based on a copy + val toRemove: List = tags.toTypedArray().filter(keyFilter) + @Suppress("UnsafeThirdPartyFunctionCall") + // NPE cannot happen here (toRemove is explicitly non null) + // ClassCastException cannot happen, we're removing objects that come from the set + tags.removeAll(toRemove) + } + + // endregion + + internal companion object { + internal const val DEFAULT_SAMPLE_RATE = 100f + internal const val SDK_NOT_INITIALIZED_WARNING_MESSAGE = + "You're trying to create a Logger instance, but the SDK was not yet initialized. " + + "This Logger will not be able to send any messages. " + + "Please initialize the Datadog SDK first before" + + " creating a new Logger instance." + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logs.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logs.kt new file mode 100644 index 0000000000..38e6a848bf --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logs.kt @@ -0,0 +1,114 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log + +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.log.internal.LogsFeature + +/** + * An entry point to Datadog Logs feature. + */ +object Logs { + + /** + * Enables a Logs feature based on the configuration provided. + * + * @param logsConfiguration Configuration to use for the feature. + * @param sdkCore SDK instance to register feature in. If not provided, default SDK instance + * will be used. + */ + @JvmOverloads + @JvmStatic + fun enable(logsConfiguration: LogsConfiguration, sdkCore: SdkCore = Datadog.getInstance()) { + val logsFeature = LogsFeature( + sdkCore = sdkCore as FeatureSdkCore, + customEndpointUrl = logsConfiguration.customEndpointUrl, + eventMapper = logsConfiguration.eventMapper + ) + + sdkCore.registerFeature(logsFeature) + } + + /** + * Identify whether a [Logs] has been enabled for the given SDK instance. + * + * This check is useful in scenarios where more than one component may be responsible + * for enabling the feature + * + * @param sdkCore the [SdkCore] instance to check against. If not provided, default instance + * will be checked. + * @return whether Logs has been enabled + */ + @JvmOverloads + @JvmStatic + fun isEnabled(sdkCore: SdkCore = Datadog.getInstance()): Boolean { + return (sdkCore as FeatureSdkCore).getFeature(Feature.LOGS_FEATURE_NAME) != null + } + + /** + * Add a custom attribute to all future logs sent by loggers created from the given SDK core. + * + * Values can be nested up to 10 levels deep. Keys + * using more than 10 levels will be sanitized by SDK. + * + * @param key the key for this attribute + * @param value the attribute value + * @param sdkCore the [SdkCore] instance to add the attribute to. If not provided, the default + * instance is used. + */ + @JvmOverloads + @JvmStatic + fun addAttribute(key: String, value: Any?, sdkCore: SdkCore = Datadog.getInstance()) { + val featureCore = sdkCore as FeatureSdkCore + val logsFeature = featureCore.getFeature(Feature.LOGS_FEATURE_NAME)?.unwrap() + if (logsFeature == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { LOGS_NOT_ENABLED_MESSAGE } + ) + return + } else { + logsFeature.addAttribute(key, value) + } + } + + /** + * Remove a custom attribute from all future logs sent by loggers created from the given SDK core. + * + * Previous logs won't lose the attribute value associated with this key if they were created + * prior to this call. + * + * @param key the key of the attribute to remove + * @param sdkCore the [SdkCore] instance to remove the attribute from. If not provided, the default + * instance is used. + */ + @JvmOverloads + @JvmStatic + fun removeAttribute(key: String, sdkCore: SdkCore = Datadog.getInstance()) { + val featureCore = sdkCore as FeatureSdkCore + val logsFeature = featureCore.getFeature(Feature.LOGS_FEATURE_NAME)?.unwrap() + if (logsFeature == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { LOGS_NOT_ENABLED_MESSAGE } + ) + return + } else { + logsFeature.removeAttribute(key) + } + } + + internal const val LOGS_NOT_ENABLED_MESSAGE = + "You're trying to add attributes to logs, but the feature is not enabled. " + + "Please enable it first." +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/LogsConfiguration.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/LogsConfiguration.kt new file mode 100644 index 0000000000..4cce40b54d --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/LogsConfiguration.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log + +import com.datadog.android.event.EventMapper +import com.datadog.android.event.NoOpEventMapper +import com.datadog.android.log.model.LogEvent + +/** + * Describes configuration to be used for the Logs feature. + */ +data class LogsConfiguration internal constructor( + internal val customEndpointUrl: String?, + internal val eventMapper: EventMapper +) { + + /** + * A Builder class for a [LogsConfiguration]. + */ + class Builder { + private var customEndpointUrl: String? = null + private var logsEventMapper: EventMapper = NoOpEventMapper() + + /** + * Let the Logs feature target a custom server. + * The provided url should be the full endpoint url, e.g.: https://example.com/logs/upload + */ + fun useCustomEndpoint(endpoint: String): Builder { + customEndpointUrl = endpoint + return this + } + + /** + * Sets the [EventMapper] for the [LogEvent]. + * You can use this interface implementation to modify the + * [LogEvent] attributes before serialisation. + * + * @param eventMapper the [EventMapper] implementation. + */ + fun setEventMapper(eventMapper: EventMapper): Builder { + logsEventMapper = eventMapper + return this + } + + /** + * Builds a [LogsConfiguration] based on the current state of this Builder. + */ + fun build(): LogsConfiguration { + return LogsConfiguration( + customEndpointUrl = customEndpointUrl, + eventMapper = logsEventMapper + ) + } + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/LogsFeature.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/LogsFeature.kt new file mode 100644 index 0000000000..dd899dd9ab --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/LogsFeature.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal + +import android.content.Context +import android.util.Log +import androidx.annotation.AnyThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureEventReceiver +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.feature.StorageBackedFeature +import com.datadog.android.api.net.RequestFactory +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.api.storage.NoOpDataWriter +import com.datadog.android.event.EventMapper +import com.datadog.android.event.MapperSerializer +import com.datadog.android.internal.utils.NULL_MAP_VALUE +import com.datadog.android.log.internal.domain.DatadogLogGenerator +import com.datadog.android.log.internal.domain.event.LogEventMapperWrapper +import com.datadog.android.log.internal.domain.event.LogEventSerializer +import com.datadog.android.log.internal.net.LogsRequestFactory +import com.datadog.android.log.internal.storage.LogsDataWriter +import com.datadog.android.log.model.LogEvent +import java.util.Locale +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Logs feature class, which needs to be registered with Datadog SDK instance. + */ +internal class LogsFeature( + private val sdkCore: FeatureSdkCore, + customEndpointUrl: String?, + internal val eventMapper: EventMapper +) : StorageBackedFeature, FeatureEventReceiver { + + internal var dataWriter: DataWriter = NoOpDataWriter() + private val initialized = AtomicBoolean(false) + internal var packageName = "" + private val logGenerator = DatadogLogGenerator() + private val attributes = ConcurrentHashMap() + + // region Context Information (attributes) + /** + * Add a custom attribute to all logs sent by any logger created from this feature. + * + * Values can be nested up to 10 levels deep. Keys + * using more than 10 levels will be sanitized by SDK. + * + * @param key the key for this attribute + * @param value the attribute value + */ + internal fun addAttribute(key: String, value: Any?) { + if (value == null) { + attributes[key] = NULL_MAP_VALUE + } else { + attributes[key] = value + } + } + + /** + * Remove a custom attribute from all future logs sent by any logger created from this feature. + * Previous logs won't lose the attribute value associated with this key if they were created + * prior to this call. + * @param key the key of the attribute to remove + */ + internal fun removeAttribute(key: String) { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + attributes.remove(key) + } + + internal fun getAttributes(): Map { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + return attributes.toMap() + } + + // endregion + + // region Feature + + override val name: String = Feature.LOGS_FEATURE_NAME + + override fun onInitialize(appContext: Context) { + sdkCore.setEventReceiver(name, this) + + packageName = appContext.packageName + + dataWriter = createDataWriter(eventMapper) + initialized.set(true) + } + + override val requestFactory: RequestFactory by lazy { + LogsRequestFactory( + customEndpointUrl, + sdkCore.internalLogger + ) + } + + override val storageConfiguration: FeatureStorageConfiguration = + FeatureStorageConfiguration.DEFAULT + + override fun onStop() { + sdkCore.removeEventReceiver(name) + dataWriter = NoOpDataWriter() + packageName = "" + initialized.set(false) + @Suppress("UnsafeThirdPartyFunctionCall") + attributes.clear() + } + + // endregion + + // region FeatureEventReceiver + + @AnyThread + override fun onReceive(event: Any) { + if (event !is Map<*, *>) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { UNSUPPORTED_EVENT_TYPE.format(Locale.US, event::class.java.canonicalName) } + ) + return + } + + if (event[TYPE_EVENT_KEY] == "span_log") { + sendSpanLog(event) + } else { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { UNKNOWN_EVENT_TYPE_PROPERTY_VALUE.format(Locale.US, event[TYPE_EVENT_KEY]) } + ) + } + } + + // endregion + + // region Internal + + private fun createDataWriter( + eventMapper: EventMapper + ): DataWriter { + return LogsDataWriter( + serializer = MapperSerializer( + LogEventMapperWrapper(eventMapper, sdkCore.internalLogger), + LogEventSerializer(sdkCore.internalLogger) + ), + internalLogger = sdkCore.internalLogger + ) + } + + private fun sendSpanLog(data: Map<*, *>) { + val timestamp = data[TIMESTAMP_EVENT_KEY] as? Long + val message = data[MESSAGE_EVENT_KEY] as? String + val loggerName = data[LOGGER_NAME_EVENT_KEY] as? String + val attributes = (data[ATTRIBUTES_EVENT_KEY] as? Map<*, *>) + ?.filterKeys { it is String } + ?.mapKeys { it.key as String } + + val logStatus = data[LOG_STATUS_EVENT_KEY] as? Int ?: Log.VERBOSE + + @Suppress("ComplexCondition") + if (loggerName == null || message == null || attributes == null || timestamp == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { SPAN_LOG_EVENT_MISSING_MANDATORY_FIELDS_WARNING } + ) + return + } + + sdkCore.getFeature(name) + ?.withWriteContext( + withFeatureContexts = setOf(Feature.RUM_FEATURE_NAME) + ) { datadogContext, writeScope -> + val log = logGenerator.generateLog( + logStatus, + datadogContext = datadogContext, + attachNetworkInfo = true, + loggerName = loggerName, + message = message, + throwable = null, + attributes = attributes, + timestamp = timestamp, + // false, because span log event will already have the necessary attributes + bundleWithTraces = false, + bundleWithRum = true, + threadName = Thread.currentThread().name, + tags = emptySet() + ) + + writeScope { + dataWriter.write(it, log, EventType.DEFAULT) + } + } + } + + // endregion + + internal companion object { + + private const val TYPE_EVENT_KEY = "type" + private const val TIMESTAMP_EVENT_KEY = "timestamp" + private const val LOGGER_NAME_EVENT_KEY = "loggerName" + private const val ATTRIBUTES_EVENT_KEY = "attributes" + private const val MESSAGE_EVENT_KEY = "message" + private const val LOG_STATUS_EVENT_KEY = "logStatus" + + internal const val UNSUPPORTED_EVENT_TYPE = + "Logs feature receive an event of unsupported type=%s." + internal const val UNKNOWN_EVENT_TYPE_PROPERTY_VALUE = + "Logs feature received an event with unknown value of \"type\" property=%s." + internal const val NDK_CRASH_EVENT_MISSING_MANDATORY_FIELDS_WARNING = + "Logs feature received a NDK crash event where" + + " one or more mandatory (loggerName, message, timestamp, attributes)" + + " fields are either missing or have wrong type." + internal const val SPAN_LOG_EVENT_MISSING_MANDATORY_FIELDS_WARNING = + "Logs feature received a Span log event where" + + " one or more mandatory (loggerName, message, timestamp, attributes)" + + " fields are either missing or have wrong type." + + internal const val MAX_WRITE_WAIT_TIMEOUT_MS = 500L + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/DatadogLogGenerator.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/DatadogLogGenerator.kt new file mode 100644 index 0000000000..a4a60590fb --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/DatadogLogGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.domain + +import com.datadog.android.api.context.AccountInfo +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.DeviceInfo +import com.datadog.android.api.context.DeviceType +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.api.feature.Feature +import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.log.LogAttributes +import com.datadog.android.log.internal.utils.buildLogDateFormat +import com.datadog.android.log.model.LogEvent +import java.util.Date + +@Suppress("TooManyFunctions") +internal class DatadogLogGenerator( + /** + * Custom service name. If not provided, value will be taken from [DatadogContext]. + */ + internal val serviceName: String? = null +) : LogGenerator { + + private val simpleDateFormat = buildLogDateFormat() + + @Suppress("LongParameterList") + override fun generateLog( + level: Int, + message: String, + throwable: Throwable?, + attributes: Map, + tags: Set, + timestamp: Long, + threadName: String, + datadogContext: DatadogContext, + attachNetworkInfo: Boolean, + loggerName: String, + bundleWithTraces: Boolean, + bundleWithRum: Boolean, + userInfo: UserInfo?, + accountInfo: AccountInfo?, + networkInfo: NetworkInfo?, + threads: List + ): LogEvent { + val mutableAttributes = attributes.toMutableMap() + val error = throwable?.let { + val fingerprint = mutableAttributes.remove(LogAttributes.ERROR_FINGERPRINT) as? String + val kind = it.javaClass.canonicalName ?: it.javaClass.simpleName + LogEvent.Error( + kind = kind, + stack = it.stackTraceToString(), + message = it.message, + fingerprint = fingerprint, + threads = threads.map { thread -> + LogEvent.Thread( + name = thread.name, + crashed = thread.crashed, + stack = thread.stack, + state = thread.state + ) + }.ifEmpty { null } + ) + } + return internalGenerateLog( + level, + message, + error, + mutableAttributes, + tags, + timestamp, + threadName, + datadogContext, + attachNetworkInfo, + loggerName, + bundleWithTraces, + bundleWithRum, + userInfo, + accountInfo, + networkInfo + ) + } + + override fun generateLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStack: String?, + attributes: Map, + tags: Set, + timestamp: Long, + threadName: String, + datadogContext: DatadogContext, + attachNetworkInfo: Boolean, + loggerName: String, + bundleWithTraces: Boolean, + bundleWithRum: Boolean, + userInfo: UserInfo?, + accountInfo: AccountInfo?, + networkInfo: NetworkInfo? + ): LogEvent { + val mutableAttributes = attributes.toMutableMap() + val error = if (errorKind != null || errorMessage != null || errorStack != null) { + val sourceType = mutableAttributes.remove(LogAttributes.SOURCE_TYPE) as? String + val fingerprint = mutableAttributes.remove(LogAttributes.ERROR_FINGERPRINT) as? String + LogEvent.Error( + kind = errorKind, + message = errorMessage, + stack = errorStack, + fingerprint = fingerprint, + sourceType = sourceType + ) + } else { + null + } + return internalGenerateLog( + level, + message, + error, + mutableAttributes, + tags, + timestamp, + threadName, + datadogContext, + attachNetworkInfo, + loggerName, + bundleWithTraces, + bundleWithRum, + userInfo, + accountInfo, + networkInfo + ) + } + + // region Internal + + @Suppress("LongParameterList") + private fun internalGenerateLog( + level: Int, + message: String, + error: LogEvent.Error?, + attributes: Map, + tags: Set, + timestamp: Long, + threadName: String, + datadogContext: DatadogContext, + attachNetworkInfo: Boolean, + loggerName: String, + bundleWithTraces: Boolean, + bundleWithRum: Boolean, + userInfo: UserInfo?, + accountInfo: AccountInfo?, + networkInfo: NetworkInfo? + ): LogEvent { + val resolvedTimestamp = timestamp + datadogContext.time.serverTimeOffsetMs + val combinedAttributes = resolveAttributes( + datadogContext, + attributes, + bundleWithTraces, + threadName, + bundleWithRum + ) + val formattedDate = synchronized(simpleDateFormat) { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + simpleDateFormat.format(Date(resolvedTimestamp)) + } + val deviceInfo = datadogContext.deviceInfo + val combinedTags = resolveTags(datadogContext, tags) + val usr = resolveUserInfo(datadogContext, userInfo) + val account = resolveAccountInfo(datadogContext, accountInfo) + val network = if (networkInfo != null || attachNetworkInfo) { + resolveNetworkInfo(datadogContext, networkInfo) + } else { + null + } + val loggerInfo = LogEvent.Logger( + name = loggerName, + threadName = threadName, + version = datadogContext.sdkVersion + ) + return LogEvent( + service = serviceName ?: datadogContext.service, + status = resolveLogLevelStatus(level), + message = message, + date = formattedDate, + // TODO RUM-3832 If NDK crash, the it should be a value from previous build + // (or whatever distinguishes debug symbols for native libs) + buildId = datadogContext.appBuildId, + error = error, + logger = loggerInfo, + dd = LogEvent.Dd( + device = LogEvent.DdDevice( + architecture = deviceInfo.architecture + ) + ), + usr = usr, + account = account, + network = network, + ddtags = combinedTags.joinToString(separator = ","), + additionalProperties = combinedAttributes, + os = resolveOsInfo(deviceInfo), + device = resolveDeviceInfo(deviceInfo) + ) + } + + private fun resolveOsInfo(deviceInfo: DeviceInfo) = LogEvent.Os( + name = deviceInfo.osName, + version = deviceInfo.osVersion, + versionMajor = deviceInfo.osMajorVersion + ) + + private fun resolveDeviceInfo(deviceInfo: DeviceInfo) = LogEvent.LogEventDevice( + type = resolveDeviceType(deviceInfo.deviceType), + name = deviceInfo.deviceName, + model = deviceInfo.deviceModel, + brand = deviceInfo.deviceBrand, + architecture = deviceInfo.architecture + ) + + private fun resolveDeviceType(deviceType: DeviceType): LogEvent.Type = when (deviceType) { + DeviceType.MOBILE -> LogEvent.Type.MOBILE + DeviceType.TABLET -> LogEvent.Type.TABLET + DeviceType.TV -> LogEvent.Type.TV + DeviceType.DESKTOP -> LogEvent.Type.DESKTOP + DeviceType.GAMING_CONSOLE -> LogEvent.Type.GAMING_CONSOLE + DeviceType.BOT -> LogEvent.Type.BOT + DeviceType.OTHER -> LogEvent.Type.OTHER + } + + private fun envTag(datadogContext: DatadogContext): String? { + val envName = datadogContext.env + return if (envName.isNotEmpty()) { + "${LogAttributes.ENV}:$envName" + } else { + null + } + } + + private fun appVersionTag(datadogContext: DatadogContext): String? { + val appVersion = datadogContext.version + return if (appVersion.isNotEmpty()) { + "${LogAttributes.APPLICATION_VERSION}:$appVersion" + } else { + null + } + } + + private fun variantTag(datadogContext: DatadogContext): String? { + val variant = datadogContext.variant + return if (variant.isNotEmpty()) { + "${LogAttributes.VARIANT}:$variant" + } else { + null + } + } + + private fun resolveNetworkInfo( + datadogContext: DatadogContext, + networkInfo: NetworkInfo? + ): LogEvent.Network { + return with(networkInfo ?: datadogContext.networkInfo) { + LogEvent.Network( + LogEvent.Client( + simCarrier = resolveSimCarrier(this), + signalStrength = strength?.toString(), + downlinkKbps = downKbps?.toString(), + uplinkKbps = upKbps?.toString(), + connectivity = connectivity.toString() + ) + ) + } + } + + private fun resolveUserInfo(datadogContext: DatadogContext, userInfo: UserInfo?): LogEvent.Usr { + return with(userInfo ?: datadogContext.userInfo) { + LogEvent.Usr( + name = name, + email = email, + id = id, + additionalProperties = additionalProperties.toMutableMap() + ) + } + } + + private fun resolveAccountInfo( + datadogContext: DatadogContext, + accountInfo: AccountInfo? + ): LogEvent.Account? { + return (accountInfo ?: datadogContext.accountInfo)?.let { + LogEvent.Account( + id = it.id, + name = it.name, + additionalProperties = it.extraInfo.toMutableMap() + ) + } + } + + private fun resolveTags( + datadogContext: DatadogContext, + tags: Set + ): MutableSet { + val combinedTags = mutableSetOf().apply { addAll(tags) } + envTag(datadogContext)?.let { + combinedTags.add(it) + } + appVersionTag(datadogContext)?.let { + combinedTags.add(it) + } + variantTag(datadogContext)?.let { + combinedTags.add(it) + } + + return combinedTags + } + + private fun resolveAttributes( + datadogContext: DatadogContext, + attributes: Map, + bundleWithTraces: Boolean, + threadName: String, + bundleWithRum: Boolean + ): MutableMap { + val combinedAttributes = mutableMapOf().apply { putAll(attributes) } + if (bundleWithTraces) { + datadogContext.featuresContext[Feature.TRACING_FEATURE_NAME]?.let { + val threadLocalContext = it["context@$threadName"] as? Map<*, *> + if (threadLocalContext != null) { + combinedAttributes[LogAttributes.DD_TRACE_ID] = threadLocalContext["trace_id"] + combinedAttributes[LogAttributes.DD_SPAN_ID] = threadLocalContext["span_id"] + } + } + } + if (bundleWithRum) { + datadogContext.featuresContext[Feature.RUM_FEATURE_NAME]?.let { + combinedAttributes[LogAttributes.RUM_APPLICATION_ID] = it["application_id"] + combinedAttributes[LogAttributes.RUM_SESSION_ID] = it["session_id"] + combinedAttributes[LogAttributes.RUM_VIEW_ID] = it["view_id"] + combinedAttributes[LogAttributes.RUM_ACTION_ID] = it["action_id"] + } + } + return combinedAttributes + } + + @Suppress("DEPRECATION") + private fun resolveLogLevelStatus(level: Int): LogEvent.Status { + return when (level) { + android.util.Log.ASSERT -> LogEvent.Status.CRITICAL + android.util.Log.ERROR -> LogEvent.Status.ERROR + android.util.Log.WARN -> LogEvent.Status.WARN + android.util.Log.INFO -> LogEvent.Status.INFO + android.util.Log.DEBUG -> LogEvent.Status.DEBUG + android.util.Log.VERBOSE -> LogEvent.Status.TRACE + DatadogLogGenerator.CRASH -> LogEvent.Status.EMERGENCY + else -> LogEvent.Status.DEBUG + } + } + + private fun resolveSimCarrier(networkInfo: NetworkInfo): LogEvent.SimCarrier? { + return if (networkInfo.carrierId != null || networkInfo.carrierName != null) { + LogEvent.SimCarrier( + id = networkInfo.carrierId?.toString(), + name = networkInfo.carrierName + ) + } else { + null + } + } + + // endregion + + companion object { + internal const val ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + internal const val CRASH: Int = 9 + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/LogGenerator.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/LogGenerator.kt new file mode 100644 index 0000000000..75ba2ebe12 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/LogGenerator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.domain + +import com.datadog.android.api.context.AccountInfo +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.log.model.LogEvent +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface LogGenerator { + + @Suppress("LongParameterList") + fun generateLog( + level: Int, + message: String, + throwable: Throwable?, + attributes: Map, + tags: Set, + timestamp: Long, + threadName: String, + datadogContext: DatadogContext, + attachNetworkInfo: Boolean, + loggerName: String, + bundleWithTraces: Boolean = true, + bundleWithRum: Boolean = true, + userInfo: UserInfo? = null, + accountInfo: AccountInfo? = null, + networkInfo: NetworkInfo? = null, + threads: List = emptyList() + ): LogEvent? + + @Suppress("LongParameterList") + fun generateLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStack: String?, + attributes: Map, + tags: Set, + timestamp: Long, + threadName: String, + datadogContext: DatadogContext, + attachNetworkInfo: Boolean, + loggerName: String, + bundleWithTraces: Boolean = true, + bundleWithRum: Boolean = true, + userInfo: UserInfo? = null, + accountInfo: AccountInfo? = null, + networkInfo: NetworkInfo? = null + ): LogEvent? +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/event/LogEventMapperWrapper.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/event/LogEventMapperWrapper.kt new file mode 100644 index 0000000000..1d47f9f15c --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/event/LogEventMapperWrapper.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.domain.event + +import com.datadog.android.api.InternalLogger +import com.datadog.android.event.EventMapper +import com.datadog.android.log.model.LogEvent +import java.util.Locale + +internal class LogEventMapperWrapper( + internal val wrappedEventMapper: EventMapper, + internal val internalLogger: InternalLogger +) : EventMapper { + + override fun map(event: LogEvent): LogEvent? { + val mappedEvent = wrappedEventMapper.map(event) + return if (mappedEvent == null) { + internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + { EVENT_NULL_WARNING_MESSAGE.format(Locale.US, event) } + ) + null + } else if (mappedEvent !== event) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { NOT_SAME_EVENT_INSTANCE_WARNING_MESSAGE.format(Locale.US, event) } + ) + null + } else { + mappedEvent + } + } + + companion object { + + internal const val EVENT_NULL_WARNING_MESSAGE = + "LogEventMapper: the returned mapped object was null. " + + "This event will be dropped: %s" + + internal const val NOT_SAME_EVENT_INSTANCE_WARNING_MESSAGE = + "LogEventMapper: the returned mapped object was not the " + + "same instance as the original object. This event will be dropped: %s" + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/event/LogEventSerializer.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/event/LogEventSerializer.kt new file mode 100644 index 0000000000..622ba145be --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/event/LogEventSerializer.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.domain.event + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.constraints.DataConstraints +import com.datadog.android.core.constraints.DatadogDataConstraints +import com.datadog.android.core.internal.utils.JsonSerializer.safeMapValuesToJson +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.log.LogAttributes +import com.datadog.android.log.model.LogEvent + +internal class LogEventSerializer( + private val internalLogger: InternalLogger, + private val dataConstraints: DataConstraints = DatadogDataConstraints(internalLogger) +) : Serializer { + + override fun serialize(model: LogEvent): String { + return sanitizeTagsAndAttributes(model).toJson().toString() + } + + private fun sanitizeTagsAndAttributes(log: LogEvent): LogEvent { + val sanitizedTags = dataConstraints + .validateTags(log.ddtags.split(",")) + .joinToString(",") + val sanitizedAttributes = dataConstraints + .validateAttributes(log.additionalProperties) + .filterKeys { it.isNotBlank() } + val usr = log.usr?.let { + val sanitizedUserAttributes = dataConstraints.validateAttributes( + it.additionalProperties, + keyPrefix = LogAttributes.USR_ATTRIBUTES_GROUP, + attributesGroupName = USER_EXTRA_GROUP_VERBOSE_NAME + ) + it.copy( + additionalProperties = sanitizedUserAttributes + .safeMapValuesToJson(internalLogger) + .toMutableMap() + ) + } + val account = log.account?.let { + val sanitizedAccountAttributes = dataConstraints.validateAttributes( + it.additionalProperties, + keyPrefix = LogAttributes.ACCOUNT_ATTRIBUTES_GROUP, + attributesGroupName = ACCOUNT_EXTRA_GROUP_VERBOSE_NAME + ) + it.copy( + additionalProperties = sanitizedAccountAttributes + .safeMapValuesToJson(internalLogger) + .toMutableMap() + ) + } + return log.copy( + ddtags = sanitizedTags, + additionalProperties = sanitizedAttributes + .safeMapValuesToJson(internalLogger) + .toMutableMap(), + usr = usr, + account = account + ) + } + + companion object { + internal const val USER_EXTRA_GROUP_VERBOSE_NAME = "user extra information" + internal const val ACCOUNT_EXTRA_GROUP_VERBOSE_NAME = "account extra information" + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandler.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandler.kt new file mode 100644 index 0000000000..bc68f950c2 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +internal class CombinedLogHandler( + internal vararg val handlers: LogHandler +) : LogHandler { + + // region LogHandler + + override fun handleLog( + level: Int, + message: String, + throwable: Throwable?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + handlers.forEach { it.handleLog(level, message, throwable, attributes, tags, timestamp) } + } + + override fun handleLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStacktrace: String?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + handlers.forEach { + it.handleLog( + level, + message, + errorKind, + errorMessage, + errorStacktrace, + attributes, + tags, + timestamp + ) + } + } + + // endregion +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandler.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandler.kt new file mode 100644 index 0000000000..4f6e43cbb0 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +internal class ConditionalLogHandler( + internal val delegateHandler: LogHandler, + internal val condition: (Int, Throwable?) -> Boolean +) : LogHandler { + override fun handleLog( + level: Int, + message: String, + throwable: Throwable?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + @Suppress("UnsafeThirdPartyFunctionCall") // internal safe call + if (condition(level, throwable)) { + delegateHandler.handleLog(level, message, throwable, attributes, tags, timestamp) + } + } + + override fun handleLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStacktrace: String?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + @Suppress("UnsafeThirdPartyFunctionCall") // internal safe call + if (condition(level, null)) { + delegateHandler.handleLog( + level, + message, + errorKind, + errorMessage, + errorStacktrace, + attributes, + tags, + timestamp + ) + } + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandler.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandler.kt new file mode 100644 index 0000000000..10372fd1b0 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.core.sampling.RateBasedSampler +import com.datadog.android.core.sampling.Sampler +import com.datadog.android.log.internal.LogsFeature +import com.datadog.android.log.internal.domain.LogGenerator +import com.datadog.android.log.model.LogEvent +import android.util.Log as AndroidLog + +internal class DatadogLogHandler( + internal val loggerName: String, + internal val logGenerator: LogGenerator, + internal val sdkCore: FeatureSdkCore, + internal val writer: DataWriter, + internal val attachNetworkInfo: Boolean, + internal val bundleWithTraces: Boolean = true, + internal val bundleWithRum: Boolean = true, + internal val sampler: Sampler = RateBasedSampler(DEFAULT_SAMPLE_RATE), + internal val minLogPriority: Int = -1 +) : LogHandler { + + // region LogHandler + + override fun handleLog( + level: Int, + message: String, + throwable: Throwable?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + if (level < minLogPriority) { + return + } + + val resolvedTimeStamp = timestamp ?: System.currentTimeMillis() + val combinedAttributes = mutableMapOf() + val logsFeature = sdkCore.getFeature(Feature.LOGS_FEATURE_NAME) + if (logsFeature != null) { + combinedAttributes.putAll(logsFeature.unwrap().getAttributes().toMutableMap()) + } + combinedAttributes.putAll(attributes) + if (sampler.sample(Unit)) { + if (logsFeature != null) { + val threadName = Thread.currentThread().name + val withFeatureContexts = mutableSetOf() + if (bundleWithRum) withFeatureContexts.add(Feature.RUM_FEATURE_NAME) + if (bundleWithTraces) withFeatureContexts.add(Feature.TRACING_FEATURE_NAME) + logsFeature.withWriteContext(withFeatureContexts) { datadogContext, writeScope -> + val log = createLog( + level, + datadogContext, + message, + throwable, + combinedAttributes, + tags, + threadName, + resolvedTimeStamp + ) + if (log != null) { + writeScope { + writer.write(it, log, EventType.DEFAULT) + } + } + } + } else { + logLogsFeatureIsNotRegistered() + } + } + + if (level >= AndroidLog.ERROR) { + val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + if (rumFeature != null) { + rumFeature.sendEvent( + mapOf( + "type" to "logger_error", + "message" to message, + "throwable" to throwable, + "attributes" to combinedAttributes + ) + ) + } else { + logRumFeatureIsNotRegistered() + } + } + } + + @Suppress("LongMethod") + override fun handleLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStacktrace: String?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + if (level < minLogPriority) { + return + } + + val resolvedTimeStamp = timestamp ?: System.currentTimeMillis() + val combinedAttributes = mutableMapOf() + val logsFeature = sdkCore.getFeature(Feature.LOGS_FEATURE_NAME) + if (logsFeature != null) { + combinedAttributes.putAll(logsFeature.unwrap().getAttributes().toMutableMap()) + } + combinedAttributes.putAll(attributes) + + if (sampler.sample(Unit)) { + if (logsFeature != null) { + val threadName = Thread.currentThread().name + val withFeatureContexts = mutableSetOf() + if (bundleWithRum) withFeatureContexts.add(Feature.RUM_FEATURE_NAME) + if (bundleWithTraces) withFeatureContexts.add(Feature.TRACING_FEATURE_NAME) + logsFeature.withWriteContext(withFeatureContexts) { datadogContext, writeScope -> + val log = createLog( + level, + datadogContext, + message, + errorKind, + errorMessage, + errorStacktrace, + combinedAttributes, + tags, + threadName, + resolvedTimeStamp + ) + if (log != null) { + writeScope { + writer.write(it, log, EventType.DEFAULT) + } + } + } + } else { + logLogsFeatureIsNotRegistered() + } + } + + if (level >= AndroidLog.ERROR) { + val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + if (rumFeature != null) { + rumFeature.sendEvent( + mapOf( + "type" to "logger_error_with_stacktrace", + "message" to message, + "stacktrace" to errorStacktrace, + "attributes" to combinedAttributes + ) + ) + } else { + logRumFeatureIsNotRegistered() + } + } + } + + // endregion + + // region Internal + + @Suppress("LongParameterList") + private fun createLog( + level: Int, + datadogContext: DatadogContext, + message: String, + throwable: Throwable?, + attributes: Map, + tags: Set, + threadName: String, + timestamp: Long + ): LogEvent? { + return logGenerator.generateLog( + level, + message, + throwable, + attributes, + tags, + timestamp, + datadogContext = datadogContext, + attachNetworkInfo = attachNetworkInfo, + loggerName = loggerName, + threadName = threadName, + bundleWithRum = bundleWithRum, + bundleWithTraces = bundleWithTraces + ) + } + + @Suppress("LongParameterList") + private fun createLog( + level: Int, + datadogContext: DatadogContext, + message: String, + errorKind: String?, + errorMessage: String?, + errorStack: String?, + attributes: Map, + tags: Set, + threadName: String, + timestamp: Long + ): LogEvent? { + return logGenerator.generateLog( + level, + message, + errorKind, + errorMessage, + errorStack, + attributes, + tags, + timestamp, + datadogContext = datadogContext, + attachNetworkInfo = attachNetworkInfo, + loggerName = loggerName, + threadName = threadName, + bundleWithRum = bundleWithRum, + bundleWithTraces = bundleWithTraces + ) + } + + private fun logLogsFeatureIsNotRegistered() { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { LOGS_FEATURE_NOT_REGISTERED } + ) + } + + private fun logRumFeatureIsNotRegistered() { + sdkCore.internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + { RUM_FEATURE_NOT_REGISTERED } + ) + } + + // endregion + + private companion object { + const val DEFAULT_SAMPLE_RATE = 100f + const val LOGS_FEATURE_NOT_REGISTERED = + "Requested to write log, but Logs feature is not registered." + const val RUM_FEATURE_NOT_REGISTERED = + "Requested to forward error log to RUM, but RUM feature is not registered." + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/LogHandler.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/LogHandler.kt new file mode 100644 index 0000000000..0867bf4447 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/LogHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +internal interface LogHandler { + + /** + * Handle the log. + * @param level the priority level (must be one of the Android Log.* constants) + * @param message the message to be logged + * @param throwable a (nullable) throwable to be logged with the message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exists in this logger, it will be overridden (just for this message) + * @param tags the tags for this message + * @param timestamp the time at which this log occurred + */ + fun handleLog( + level: Int, + message: String, + throwable: Throwable? = null, + attributes: Map = emptyMap(), + tags: Set = emptySet(), + timestamp: Long? = null + ) + + /** + * Handle the log. + * @param level the priority level (must be one of the Android Log.* constants) + * @param message the message to be logged + * @param errorKind the kind of error to be logged with the message + * @param errorMessage the message from the error to be logged with this message + * @param errorStacktrace the stack trace from the error to be logged with this message + * @param attributes a map of attributes to include only for this message. If an attribute with + * the same key already exists in this logger, it will be overridden (just for this message) + * @param tags the tags for this message + * @param timestamp the time at which this log occurred + */ + fun handleLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStacktrace: String?, + attributes: Map = emptyMap(), + tags: Set = emptySet(), + timestamp: Long? = null + ) +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandler.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandler.kt new file mode 100644 index 0000000000..96af5c2d47 --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandler.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +import android.os.Build +import android.util.Log +import com.datadog.android.log.Logger + +internal class LogcatLogHandler( + internal val serviceName: String, + internal val useClassnameAsTag: Boolean, + internal val isDebug: Boolean = false +) : LogHandler { + + // region LogHandler + + override fun handleLog( + level: Int, + message: String, + throwable: Throwable?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + val stackElement = getCallerStackElement() + val tag = resolveTag(stackElement) + val suffix = resolveSuffix(stackElement) + Log.println(level, tag, message + suffix) + if (throwable != null) { + Log.println( + level, + tag, + Log.getStackTraceString(throwable) + ) + } + } + + override fun handleLog( + level: Int, + message: String, + errorKind: String?, + errorMessage: String?, + errorStacktrace: String?, + attributes: Map, + tags: Set, + timestamp: Long? + ) { + val stackElement = getCallerStackElement() + val tag = resolveTag(stackElement) + val suffix = resolveSuffix(stackElement) + Log.println(level, tag, message + suffix) + if (errorStacktrace != null) { + Log.println( + level, + tag, + errorStacktrace + ) + } + } + + // endregion + + // region Internal + + private fun resolveTag(stackTraceElement: StackTraceElement?): String { + val tag = if (stackTraceElement == null) { + serviceName + } else { + stackTraceElement.className + .replace(ANONYMOUS_CLASS, "") + .substringAfterLast('.') + } + return if (tag.length >= MAX_TAG_LENGTH && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + @Suppress("UnsafeThirdPartyFunctionCall") + // substring can't throw IndexOutOfBounds, we checked the length + tag.substring(0, MAX_TAG_LENGTH) + } else { + tag + } + } + + private fun resolveSuffix(stackTraceElement: StackTraceElement?): String { + return if (stackTraceElement == null) { + "" + } else { + "\t| at .${stackTraceElement.methodName}" + + "(${stackTraceElement.fileName}:${stackTraceElement.lineNumber})" + } + } + + @Suppress("ThrowingExceptionsWithoutMessageOrCause") + internal fun getCallerStackElement(): StackTraceElement? { + return if (isDebug && useClassnameAsTag) { + val stackTrace = Throwable().stackTrace + return findValidCallStackElement(stackTrace) + } else { + null + } + } + + internal fun findValidCallStackElement( + stackTrace: Array + ): StackTraceElement? { + return stackTrace.firstOrNull { element -> + element.className !in IGNORED_CLASS_NAMES && + IGNORED_PACKAGE_PREFIXES.none { element.className.startsWith(it) } + } + } + + // endregion + + companion object { + + private const val MAX_TAG_LENGTH = 23 + + private val ANONYMOUS_CLASS = Regex("(\\$\\d+)+$") + + // internal for testing + internal val IGNORED_CLASS_NAMES = arrayOf( + Logger::class.java.canonicalName, + LogHandler::class.java.canonicalName, + LogHandler::class.java.canonicalName?.plus("\$DefaultImpls"), + LogcatLogHandler::class.java.canonicalName, + ConditionalLogHandler::class.java.canonicalName, + CombinedLogHandler::class.java.canonicalName, + DatadogLogHandler::class.java.canonicalName + ) + + // internal for testing + internal val IGNORED_PACKAGE_PREFIXES = arrayOf( + "com.datadog.android.timber", + "timber.log" + ) + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/net/LogsRequestFactory.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/net/LogsRequestFactory.kt new file mode 100644 index 0000000000..4e3b27caef --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/net/LogsRequestFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.net + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.net.Request +import com.datadog.android.api.net.RequestExecutionContext +import com.datadog.android.api.net.RequestFactory +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.utils.join +import java.util.Locale +import java.util.UUID + +/** + * Request factory for the Logs feature. + * @param customEndpointUrl URL of the Logs intake. + * @param internalLogger logger to use. + */ +internal class LogsRequestFactory( + internal val customEndpointUrl: String?, + private val internalLogger: InternalLogger +) : RequestFactory { + + /** @inheritdoc */ + override fun create( + context: DatadogContext, + executionContext: RequestExecutionContext, + batchData: List, + batchMetadata: ByteArray? + ): Request? { + val requestId = UUID.randomUUID().toString() + + return Request( + id = requestId, + description = "Logs Request", + url = buildUrl(context.source, context), + headers = buildHeaders( + requestId, + context.clientToken, + context.source, + context.sdkVersion + ), + body = batchData.map { it.data } + .join( + separator = PAYLOAD_SEPARATOR, + prefix = PAYLOAD_PREFIX, + suffix = PAYLOAD_SUFFIX, + internalLogger = internalLogger + ), + contentType = RequestFactory.CONTENT_TYPE_JSON + ) + } + + private fun buildUrl(source: String, context: DatadogContext): String { + val baseUrl = customEndpointUrl ?: (context.site.intakeEndpoint + "/api/v2/logs") + return "%s?%s=%s" + .format( + Locale.US, + baseUrl, + RequestFactory.QUERY_PARAM_SOURCE, + source + ) + } + + private fun buildHeaders( + requestId: String, + clientToken: String, + source: String, + sdkVersion: String + ): Map { + return mapOf( + RequestFactory.HEADER_API_KEY to clientToken, + RequestFactory.HEADER_EVP_ORIGIN to source, + RequestFactory.HEADER_EVP_ORIGIN_VERSION to sdkVersion, + RequestFactory.HEADER_REQUEST_ID to requestId + ) + } + + companion object { + private val PAYLOAD_SEPARATOR = ",".toByteArray(Charsets.UTF_8) + private val PAYLOAD_PREFIX = "[".toByteArray(Charsets.UTF_8) + private val PAYLOAD_SUFFIX = "]".toByteArray(Charsets.UTF_8) + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/storage/LogsDataWriter.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/storage/LogsDataWriter.kt new file mode 100644 index 0000000000..ef522548af --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/storage/LogsDataWriter.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.storage + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.core.persistence.serializeToByteArray +import com.datadog.android.log.model.LogEvent + +internal class LogsDataWriter( + internal val serializer: Serializer, + private val internalLogger: InternalLogger +) : DataWriter { + + @WorkerThread + override fun write(writer: EventBatchWriter, element: LogEvent, eventType: EventType): Boolean { + val serialized = serializer.serializeToByteArray(element, internalLogger) ?: return false + return synchronized(this) { + writer.write(RawBatchEvent(data = serialized), batchMetadata = null, eventType = eventType) + } + } +} diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/utils/LogUtils.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/utils/LogUtils.kt new file mode 100644 index 0000000000..337f0358dc --- /dev/null +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/utils/LogUtils.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.utils + +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +internal const val ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + +@Suppress("UnsafeThirdPartyFunctionCall") +internal fun buildLogDateFormat(): SimpleDateFormat = + // NPE cannot happen here, ISO_8601 pattern is valid + SimpleDateFormat(ISO_8601, Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LoggerBuilderTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LoggerBuilderTest.kt new file mode 100644 index 0000000000..d365b5f910 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LoggerBuilderTest.kt @@ -0,0 +1,255 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.core.sampling.RateBasedSampler +import com.datadog.android.log.internal.LogsFeature +import com.datadog.android.log.internal.domain.DatadogLogGenerator +import com.datadog.android.log.internal.logger.CombinedLogHandler +import com.datadog.android.log.internal.logger.DatadogLogHandler +import com.datadog.android.log.internal.logger.LogHandler +import com.datadog.android.log.internal.logger.LogcatLogHandler +import com.datadog.android.log.internal.logger.NoOpLogHandler +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class LoggerBuilderTest { + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + @Mock + lateinit var mockLogsFeatureScope: FeatureScope + + @Mock + lateinit var mockLogsFeature: LogsFeature + + @Mock + lateinit var mockDataWriter: DataWriter + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @StringForgery + lateinit var fakeServiceName: String + + @StringForgery(regex = "[a-z]{2,4}(\\.[a-z]{3,8}){2,4}") + lateinit var fakePackageName: String + + @BeforeEach + fun `set up`() { + whenever(mockLogsFeature.packageName) doReturn fakePackageName + whenever(mockLogsFeature.dataWriter) doReturn mockDataWriter + whenever(mockSdkCore.service) doReturn fakeServiceName + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn mockLogsFeatureScope + whenever(mockLogsFeatureScope.unwrap()) doReturn mockLogsFeature + } + + @Test + fun `builder returns no-op if logs feature is missing`() { + // Given + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn null + + // When + val testedLogger = Logger.Builder(mockSdkCore).build() + + // Then + val handler = testedLogger.handler + assertThat(handler).isInstanceOf(NoOpLogHandler::class.java) + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(firstValue()).isEqualTo(Logger.SDK_NOT_INITIALIZED_WARNING_MESSAGE) + } + } + + @Test + fun `builder without custom settings uses defaults`() { + val logger = Logger.Builder(mockSdkCore) + .build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + assertThat(handler.writer).isSameAs(mockDataWriter) + assertThat(handler.bundleWithTraces).isTrue + assertThat(handler.sampler).isInstanceOf(RateBasedSampler::class.java) + assertThat((handler.sampler as RateBasedSampler).getSampleRate()).isEqualTo(100.0f) + assertThat(handler.minLogPriority).isEqualTo(-1) + assertThat(handler.loggerName).isEqualTo(fakePackageName) + assertThat(handler.attachNetworkInfo).isFalse + + val logGenerator: DatadogLogGenerator = handler.logGenerator as DatadogLogGenerator + assertThat(logGenerator.serviceName).isEqualTo(fakeServiceName) + } + + @Test + fun `builder can set a service name`(forge: Forge) { + val serviceName = forge.anAlphabeticalString() + + val logger = Logger.Builder(mockSdkCore) + .setService(serviceName) + .build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + val logGenerator: DatadogLogGenerator = handler.logGenerator as DatadogLogGenerator + assertThat(logGenerator.serviceName).isEqualTo(serviceName) + } + + @Test + fun `builder can disable datadog logs`() { + val logger: Logger = Logger.Builder(mockSdkCore) + .setRemoteSampleRate(0f) + .build() + + val handler: LogHandler = logger.handler + assertThat(handler).isInstanceOf(NoOpLogHandler::class.java) + } + + @Test + fun `builder can set min datadog logs priority`( + @IntForgery minLogThreshold: Int + ) { + val logger: Logger = Logger.Builder(mockSdkCore) + .setRemoteLogThreshold(minLogThreshold) + .build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + assertThat(handler.minLogPriority).isEqualTo(minLogThreshold) + } + + @Test + fun `builder can enable logcat logs`() { + val logcatLogsEnabled = true + + val logger = Logger.Builder(mockSdkCore) + .setLogcatLogsEnabled(logcatLogsEnabled) + .build() + + val handler: LogHandler = logger.handler + assertThat(handler).isInstanceOf(CombinedLogHandler::class.java) + val handlers = (handler as CombinedLogHandler).handlers + assertThat(handlers) + .hasAtLeastOneElementOfType(LogcatLogHandler::class.java) + .hasAtLeastOneElementOfType(DatadogLogHandler::class.java) + } + + @Test + fun `builder can enable only logcat logs`( + forge: Forge + ) { + val logcatLogsEnabled = true + val fakeServiceName = forge.anAlphaNumericalString() + + val logger = Logger.Builder(mockSdkCore) + .setRemoteSampleRate(0f) + .setLogcatLogsEnabled(logcatLogsEnabled) + .setService(fakeServiceName) + .build() + + val handler: LogHandler = logger.handler + assertThat(handler).isInstanceOf(LogcatLogHandler::class.java) + val logcatLogHandler = handler as LogcatLogHandler + assertThat(logcatLogHandler.serviceName) + .isEqualTo(fakeServiceName) + assertThat(logcatLogHandler.useClassnameAsTag) + .isTrue + } + + @Test + fun `builder can enable network info`() { + val networkInfoEnabled = true + + val logger = Logger.Builder(mockSdkCore) + .setNetworkInfoEnabled(networkInfoEnabled) + .build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + assertThat(handler.attachNetworkInfo).isTrue + } + + @Test + fun `builder can set the logger name`(forge: Forge) { + val loggerName = forge.anAlphabeticalString() + + val logger = Logger.Builder(mockSdkCore) + .setName(loggerName) + .build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + assertThat(handler.loggerName).isEqualTo(loggerName) + } + + @Test + fun `builder can disable the bundle with trace feature`() { + val logger = Logger.Builder(mockSdkCore) + .setBundleWithTraceEnabled(false) + .build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + assertThat(handler.bundleWithTraces).isFalse + } + + @Test + fun `builder can disable the bundle with rum feature`() { + val logger = Logger.Builder(mockSdkCore) + .setBundleWithRumEnabled(false) + .build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + assertThat(handler.bundleWithRum).isFalse + } + + @Test + fun `builder can set a sample rate`(forge: Forge) { + val expectedSampleRate = forge.aFloat(min = 0.0f, max = 100.0f) + + val logger = Logger.Builder(mockSdkCore).setRemoteSampleRate(expectedSampleRate).build() + + val handler: DatadogLogHandler = logger.handler as DatadogLogHandler + val sampler = handler.sampler + assertThat(sampler).isInstanceOf(RateBasedSampler::class.java) + assertThat((sampler as RateBasedSampler).getSampleRate()).isEqualTo(expectedSampleRate) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LoggerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LoggerTest.kt new file mode 100644 index 0000000000..d45627787f --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LoggerTest.kt @@ -0,0 +1,1141 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log + +import android.util.Log +import com.datadog.android.internal.utils.NULL_MAP_VALUE +import com.datadog.android.log.internal.logger.LogHandler +import com.datadog.android.utils.forge.Configurator +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.json.JSONArray +import org.json.JSONObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.isNull +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import java.util.Date +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings +@ForgeConfiguration(Configurator::class) +internal class LoggerTest { + + lateinit var testedLogger: Logger + + @Mock + lateinit var mockLogHandler: LogHandler + + lateinit var fakeMessage: String + + @BeforeEach + fun `set up`(forge: Forge) { + fakeMessage = forge.anAlphabeticalString() + testedLogger = Logger(mockLogHandler) + } + + // region Log + + @Test + fun `logger logs message with verbose level`() { + testedLogger.v(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.VERBOSE, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `logger logs message with verbose level and custom timestamp`(forge: Forge) { + val timestamp = forge.aLong() + val level = forge.anInt() + testedLogger.internalLog(level, fakeMessage, null, emptyMap(), timestamp) + + verify(mockLogHandler) + .handleLog( + level, + fakeMessage, + null, + emptyMap(), + emptySet(), + timestamp + ) + } + + @Test + fun `logger logs message with debug level`() { + testedLogger.d(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.DEBUG, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `logger logs message with info level`() { + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `logger logs message with warning level`() { + testedLogger.w(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.WARN, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `logger logs message with error level`() { + testedLogger.e(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.ERROR, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `logger logs message with assert level`() { + testedLogger.wtf(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.ASSERT, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + // endregion + + // region throwable + @Test + fun `log verbose with exception`(@Forgery throwable: Throwable) { + testedLogger.v(fakeMessage, throwable) + + verify(mockLogHandler) + .handleLog( + Log.VERBOSE, + fakeMessage, + throwable, + emptyMap(), + emptySet() + ) + } + + @Test + fun `log debug with exception`(@Forgery throwable: Throwable) { + testedLogger.d(fakeMessage, throwable) + + verify(mockLogHandler) + .handleLog( + Log.DEBUG, + fakeMessage, + throwable, + emptyMap(), + emptySet() + ) + } + + @Test + fun `log info with exception`(@Forgery throwable: Throwable) { + testedLogger.i(fakeMessage, throwable) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + throwable, + emptyMap(), + emptySet() + ) + } + + @Test + fun `log warning with exception`(@Forgery throwable: Throwable) { + testedLogger.w(fakeMessage, throwable) + + verify(mockLogHandler) + .handleLog( + Log.WARN, + fakeMessage, + throwable, + emptyMap(), + emptySet() + ) + } + + @Test + fun `log error with exception`(@Forgery throwable: Throwable) { + testedLogger.e(fakeMessage, throwable) + + verify(mockLogHandler) + .handleLog( + Log.ERROR, + fakeMessage, + throwable, + emptyMap(), + emptySet() + ) + } + + @Test + fun `log wtf with exception`(@Forgery throwable: Throwable) { + testedLogger.wtf(fakeMessage, throwable) + + verify(mockLogHandler) + .handleLog( + Log.ASSERT, + fakeMessage, + throwable, + emptyMap(), + emptySet() + ) + } + + @Test + fun `log with expanded error info`( + @StringForgery errorKind: String, + @StringForgery errorMessage: String, + @StringForgery errorStack: String + ) { + testedLogger.log( + Log.DEBUG, + fakeMessage, + errorKind, + errorMessage, + errorStack + ) + + verify(mockLogHandler) + .handleLog( + Log.DEBUG, + fakeMessage, + errorKind, + errorMessage, + errorStack, + emptyMap(), + emptySet() + ) + } + + // endregion + + // region addAttribute + + @Test + fun `add boolean attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aBool() + + testedLogger.addAttribute(key, value) + testedLogger.v(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.VERBOSE, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add int attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.anInt() + + testedLogger.addAttribute(key, value) + testedLogger.d(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.DEBUG, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add long attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aLong() + + testedLogger.addAttribute(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add float attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aFloat() + + testedLogger.addAttribute(key, value) + testedLogger.w(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.WARN, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add double attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aDouble() + + testedLogger.addAttribute(key, value) + testedLogger.e(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.ERROR, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add String attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aNumericalString() + + testedLogger.addAttribute(key, value) + testedLogger.wtf(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.ASSERT, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add null String attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value: String? = null + + testedLogger.addAttribute(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to NULL_MAP_VALUE), + emptySet() + ) + } + + @Test + fun `add Date attribute to logger`(forge: Forge, @Forgery value: Date) { + val key = forge.anAlphabeticalString() + + testedLogger.addAttribute(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add GSON JsonObject attribute to logger`(forge: Forge, @Forgery value: JsonObject) { + val key = forge.anAlphabeticalString() + + testedLogger.addAttribute(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add GSON JsonArray attribute to logger`(forge: Forge, @Forgery value: JsonArray) { + val key = forge.anAlphabeticalString() + + testedLogger.addAttribute(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add OrgJson JSONObject attribute to logger`(forge: Forge, @Forgery value: JSONObject) { + val key = forge.anAlphabeticalString() + + testedLogger.addAttribute(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `add OrgJson JSONArray attribute to logger`(forge: Forge, @Forgery value: JSONArray) { + val key = forge.anAlphabeticalString() + + testedLogger.addAttribute(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `attributes are passed through to expanded error log`( + forge: Forge, + @StringForgery errorKind: String, + @StringForgery errorStack: String + ) { + // GIVEN + val boolKey = forge.anAlphabeticalString() + val fakeBoolean = forge.aBool() + testedLogger.addAttribute(boolKey, fakeBoolean) + + val stringKey = forge.anAlphabeticalString() + val fakeString = forge.anAlphabeticalString() + testedLogger.addAttribute(stringKey, fakeString) + + // WHEN + testedLogger.log( + Log.INFO, + fakeMessage, + errorKind, + null, + errorStack + ) + + // THEN + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + errorKind, + null, + errorStack, + mapOf( + boolKey to fakeBoolean, + stringKey to fakeString + ) + ) + } + + // endregion + + // region removeAttribute + + @Test + fun `remove boolean attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aBool() + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.v(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.VERBOSE, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove int attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.anInt() + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.d(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.DEBUG, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove long attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aLong() + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove float attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aFloat() + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.w(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.WARN, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove double attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aDouble() + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.e(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.ERROR, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove null String attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value: String? = null + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.wtf(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.ASSERT, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove String attribute to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aNumericalString() + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove Date attribute to logger`(forge: Forge, @Forgery value: Date) { + val key = forge.anAlphabeticalString() + + testedLogger.addAttribute(key, value) + testedLogger.removeAttribute(key) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + // endregion + + // region Local Attributes + + @Test + fun `log message with local attributes`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.anInt() + + testedLogger.v(fakeMessage, null, mapOf(key to value)) + + verify(mockLogHandler) + .handleLog( + Log.VERBOSE, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `log message with local attributes and timestamp`(forge: Forge) { + val timestamp = forge.aLong() + val key = forge.anAlphabeticalString() + val value = forge.anInt() + val level = forge.anInt() + + testedLogger.internalLog(level, fakeMessage, null, mapOf(key to value), timestamp) + + verify(mockLogHandler) + .handleLog( + level, + fakeMessage, + null, + mapOf(key to value), + emptySet(), + timestamp + ) + } + + @Test + fun `log message with local attributes (null value)`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value: Any? = null + + testedLogger.d(fakeMessage, null, mapOf(key to value)) + + verify(mockLogHandler) + .handleLog( + Log.DEBUG, + fakeMessage, + null, + mapOf(key to value), + emptySet() + ) + } + + @Test + fun `log message with local attributes override logger value`(forge: Forge) { + val key = forge.anAlphabeticalString() + val loggerValue = forge.aFloat() + val localValue = forge.anAlphabeticalString() + + testedLogger.addAttribute(key, loggerValue) + testedLogger.i(fakeMessage, null, mapOf(key to localValue)) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + mapOf(key to localValue), + emptySet() + ) + } + + @Test + fun `log message without local attributes after message with local attributes`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.anInt() + val message1 = forge.anAlphabeticalString() + val message2 = forge.anAlphabeticalString() + + testedLogger.w(message1, null, mapOf(key to value)) + testedLogger.e(message2) + + inOrder(mockLogHandler) { + verify(mockLogHandler) + .handleLog( + eq(Log.WARN), + eq(message1), + isNull(), + eq(mapOf(key to value)), + eq(emptySet()), + isNull() + ) + verify(mockLogHandler) + .handleLog( + eq(Log.ERROR), + eq(message2), + isNull(), + eq(emptyMap()), + eq(emptySet()), + isNull() + ) + } + } + + // endregion + + // region Tags + + @Test + fun `add simple tag to logger`(forge: Forge) { + val tag = forge.anAlphabeticalString() + + testedLogger.addTag(tag) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + setOf(tag) + ) + } + + @Test + fun `add key-value tag to logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.aNumericalString() + + testedLogger.addTag(key, value) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + setOf("$key:$value") + ) + } + + @Test + fun `add multiple tags with same key`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value1 = forge.anAlphabeticalString() + val value2 = forge.anAlphabeticalString() + val value3 = forge.anAlphabeticalString() + + testedLogger.addTag(key, value1) + testedLogger.addTag(key, value2) + testedLogger.addTag(key, value3) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + setOf("$key:$value1", "$key:$value2", "$key:$value3") + ) + } + + @Test + fun `M use copy of tags W log`( + @IntForgery(min = 0) priority: Int, + @StringForgery key: String, + @StringForgery value: String + ) { + // Given + testedLogger.addTag(key, value) + + // When + testedLogger.log(priority, fakeMessage) + + argumentCaptor> { + verify(mockLogHandler) + .handleLog( + eq(priority), + eq(fakeMessage), + isNull(), + eq(emptyMap()), + capture(), + timestamp = anyOrNull() + ) + + assertThat(lastValue).isNotSameAs(testedLogger.tags) + assertThat(lastValue).isEqualTo(testedLogger.tags) + } + } + + @Test + fun `M use copy of tags W log { error as string }`( + @IntForgery(min = 0) priority: Int, + @StringForgery key: String, + @StringForgery value: String, + @StringForgery errorMessage: String, + @StringForgery errorStack: String, + @StringForgery errorKind: String + ) { + // Given + testedLogger.addTag(key, value) + + // When + testedLogger.log(priority, fakeMessage, errorKind, errorMessage, errorStack) + + argumentCaptor> { + verify(mockLogHandler) + .handleLog( + eq(priority), + eq(fakeMessage), + eq(errorKind), + eq(errorMessage), + eq(errorStack), + eq(emptyMap()), + capture(), + timestamp = anyOrNull() + ) + + assertThat(lastValue).isNotSameAs(testedLogger.tags) + assertThat(lastValue).isEqualTo(testedLogger.tags) + } + } + + // endregion + + // region Remove Tags + + @Test + fun `remove tag from logger`(forge: Forge) { + val tag = forge.anAlphabeticalString() + + testedLogger.addTag(tag) + testedLogger.removeTag(tag) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove tag with key from logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value = forge.anAlphabeticalString() + + testedLogger.addTag(key, value) + testedLogger.removeTagsWithKey(key) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `remove all tags with key from logger`(forge: Forge) { + val key = forge.anAlphabeticalString() + val value1 = forge.anAlphabeticalString() + val value2 = forge.anAlphabeticalString() + val value3 = forge.anAlphabeticalString() + + testedLogger.addTag(key, value1) + testedLogger.addTag(key, value2) + testedLogger.addTag(key, value3) + testedLogger.removeTagsWithKey(key) + testedLogger.i(fakeMessage) + + verify(mockLogHandler) + .handleLog( + Log.INFO, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + // endregion + + // region Multi Thread Access + + @Test + fun `adding and removing tags is thread safe`(forge: Forge) { + val asyncOperations = 100 + val syncOperations = 10 + val randomTags = + forge.aList(asyncOperations) { + "${forge.aString(syncOperations)}:${forge.aString(syncOperations)}" + } + val countDownLatch = CountDownLatch(asyncOperations) + var logDebugExecutionCalls = 0 + repeat(asyncOperations) { + val closure = when (forge.anInt(min = 0, max = 3)) { + 0 -> { + { + repeat(syncOperations) { + repeat(syncOperations) { + val randomTagIndex = forge.anInt(0, asyncOperations) + testedLogger.addTag(randomTags[randomTagIndex]) + } + } + } + } + + 1 -> { + { + repeat(syncOperations) { + val randomTagIndex = forge.anInt(0, asyncOperations) + testedLogger.removeTag(randomTags[randomTagIndex]) + } + } + } + + 2 -> { + { + repeat(syncOperations) { + val randomTagIndex = forge.anInt(0, asyncOperations) + val tagKey = randomTags[randomTagIndex].split(":").first() + testedLogger.removeTagsWithKey(tagKey) + } + } + } + + 3 -> { + logDebugExecutionCalls++ + { + val attributes = + forge.aMap(size = forge.anInt(min = 1, max = 5)) { + forge.aString(size = syncOperations) to + forge.aString(size = syncOperations) + } + testedLogger.d( + forge.aString(size = syncOperations), + attributes = attributes + ) + } + } + + else -> { + { } + } + } + async(countDownLatch, closure) + } + + countDownLatch.await(5, TimeUnit.SECONDS) + verify(mockLogHandler, times(logDebugExecutionCalls)).handleLog( + eq(Log.DEBUG), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `adding and removing attributes is thread safe`(forge: Forge) { + val asyncOperations = 100 + val syncedOperations = 10 + val randomAttributes = forge.aList(size = asyncOperations) { + forge.aString(syncedOperations) to forge.aString(syncedOperations) + } + val countDownLatch = CountDownLatch(asyncOperations) + var logDebugExecutionCalls = 0 + repeat(asyncOperations) { + val closure = when (forge.anInt(min = 0, max = 2)) { + 0 -> { + { + repeat(syncedOperations) { + val randomAttributeIndex = forge.anInt(0, asyncOperations) + testedLogger.addAttribute( + randomAttributes[randomAttributeIndex].first, + randomAttributes[randomAttributeIndex].second + ) + } + } + } + + 1 -> { + { + repeat(syncedOperations) { + val randomAttributeIndex = forge.anInt(0, asyncOperations) + testedLogger.removeAttribute( + randomAttributes[randomAttributeIndex].first + ) + } + } + } + + 2 -> { + logDebugExecutionCalls++ + { + val attributes = + forge.aMap(size = forge.anInt(min = 1, max = 5)) { + forge.aString(size = syncedOperations) to + forge.aString(size = syncedOperations) + } + testedLogger.d( + forge.aString(size = syncedOperations), + attributes = attributes + ) + } + } + + else -> { + { } + } + } + async(countDownLatch, closure) + } + + countDownLatch.await(5, TimeUnit.SECONDS) + verify(mockLogHandler, times(logDebugExecutionCalls)).handleLog( + eq(Log.DEBUG), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + // endregion + + // region internal + + private fun async(countDownLatch: CountDownLatch, closure: () -> Unit) { + Thread { + closure() + countDownLatch.countDown() + }.start() + } + + // endregion +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LogsConfigurationBuilderTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LogsConfigurationBuilderTest.kt new file mode 100644 index 0000000000..0e8ff71da1 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LogsConfigurationBuilderTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log + +import com.datadog.android.event.EventMapper +import com.datadog.android.event.NoOpEventMapper +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(ForgeExtension::class), + ExtendWith(MockitoExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class LogsConfigurationBuilderTest { + + private val testedBuilder: LogsConfiguration.Builder = LogsConfiguration.Builder() + + @Test + fun `M use sensible defaults W build()`() { + // When + val logsConfiguration = testedBuilder.build() + + // Then + assertThat(logsConfiguration.customEndpointUrl).isNull() + assertThat(logsConfiguration.eventMapper).isInstanceOf(NoOpEventMapper::class.java) + } + + @Test + fun `M build configuration with custom site W useCustomEndpoint() and build()`( + @StringForgery(regex = "https://[a-z]+\\.com(/[a-z]+)+") logsEndpointUrl: String + ) { + // When + val logsConfiguration = testedBuilder.useCustomEndpoint(logsEndpointUrl).build() + + // Then + assertThat(logsConfiguration.customEndpointUrl).isEqualTo(logsEndpointUrl) + assertThat(logsConfiguration.eventMapper).isInstanceOf(NoOpEventMapper::class.java) + } + + @Test + fun `M build configuration with Log eventMapper W setEventMapper() and build()`() { + // Given + val mockEventMapper: EventMapper = mock() + + // When + val logsConfiguration = testedBuilder + .setEventMapper(mockEventMapper) + .build() + + // Then + assertThat(logsConfiguration.customEndpointUrl).isNull() + assertThat(logsConfiguration.eventMapper).isEqualTo(mockEventMapper) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LogsTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LogsTest.kt new file mode 100644 index 0000000000..e695860d77 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/LogsTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.log.internal.LogsFeature +import com.datadog.android.log.internal.net.LogsRequestFactory +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class LogsTest { + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + @BeforeEach + fun `set up`() { + whenever(mockSdkCore.internalLogger) doReturn mock() + } + + @Test + fun `M register logs feature W enable()`( + @StringForgery fakePackageName: String, + @Forgery fakeLogsConfiguration: LogsConfiguration + ) { + // When + Logs.enable(fakeLogsConfiguration, mockSdkCore) + + // Then + argumentCaptor { + verify(mockSdkCore).registerFeature(capture()) + + lastValue.onInitialize( + appContext = mock { whenever(it.packageName) doReturn fakePackageName } + ) + assertThat(lastValue.eventMapper).isEqualTo(fakeLogsConfiguration.eventMapper) + assertThat((lastValue.requestFactory as LogsRequestFactory).customEndpointUrl) + .isEqualTo(fakeLogsConfiguration.customEndpointUrl) + } + } + + @Test + fun `M return true W isEnabled() { core returns feature }`() { + // Given + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn mock() + + // When + val result = Logs.isEnabled(mockSdkCore) + + // Then + assertThat(result).isTrue + } + + @Test + fun `M return false W isEnabled() { core returns null }`() { + // Given + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn null + + // When + val result = Logs.isEnabled(mockSdkCore) + + // Then + assertThat(result).isFalse + } + + @Test + fun `M log user error W addAttribute { logs not enabled }`( + @StringForgery key: String, + @StringForgery value: String + ) { + // Given + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn null + + // When + Logs.addAttribute(key, value, mockSdkCore) + + // Then + argumentCaptor<() -> String> { + verify(mockSdkCore.internalLogger).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(lastValue()).isEqualTo(Logs.LOGS_NOT_ENABLED_MESSAGE) + } + } + + @Test + fun `M log user error W removeAttribute { logs not enabled }`( + @StringForgery key: String + ) { + // Given + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn null + + // When + Logs.removeAttribute(key, mockSdkCore) + + // Then + argumentCaptor<() -> String> { + verify(mockSdkCore.internalLogger).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(lastValue()).isEqualTo(Logs.LOGS_NOT_ENABLED_MESSAGE) + } + } + + @Test + fun `M forward attributes to Feature W addAttribute`( + @StringForgery key: String, + @StringForgery value: String + ) { + // Given + val mockFeatureScope: FeatureScope = mock() + val mockLogsFeature: LogsFeature = mock() + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn mockFeatureScope + whenever(mockFeatureScope.unwrap()) doReturn mockLogsFeature + + // When + Logs.addAttribute(key, value, mockSdkCore) + + // Then + verify(mockLogsFeature).addAttribute(key, value) + } + + @Test + fun `M forward remove attributes to Feature W removeAttribute`( + @StringForgery key: String + ) { + // Given + val mockFeatureScope: FeatureScope = mock() + val mockLogsFeature: LogsFeature = mock() + whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn mockFeatureScope + whenever(mockFeatureScope.unwrap()) doReturn mockLogsFeature + + // When + Logs.removeAttribute(key, mockSdkCore) + + // Then + verify(mockLogsFeature).removeAttribute(key) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/assertj/JsonObjectAssertExt.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/assertj/JsonObjectAssertExt.kt new file mode 100644 index 0000000000..0c7d14ecbc --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/assertj/JsonObjectAssertExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.assertj + +import com.datadog.android.core.internal.utils.JsonSerializer +import com.datadog.android.internal.utils.NULL_MAP_VALUE +import com.datadog.tools.unit.assertj.JsonObjectAssert +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import org.json.JSONArray +import org.json.JSONObject +import java.util.Date + +fun JsonObjectAssert.containsExtraAttributes( + attributes: Map, + keyNamePrefix: String = "" +) { + attributes.filter { it.key.isNotBlank() } + .forEach { + val value = it.value + val key = keyNamePrefix + it.key + when (value) { + NULL_MAP_VALUE -> hasNullField(key) + null -> hasNullField(key) + is Boolean -> hasField(key, value) + is Int -> hasField(key, value) + is Long -> hasField(key, value) + is Float -> hasField(key, value) + is Double -> hasField(key, value) + is String -> hasField(key, value) + is Date -> hasField(key, value.time) + is JsonObject -> hasField(key, value) + is JsonArray -> hasField(key, value) + is Iterable<*> -> hasField(key, value.toJsonArray()) + is Map<*, *> -> hasField(key, value.toJsonObject()) + is JSONArray -> hasField(key, value.toJsonArray()) + is JSONObject -> hasField(key, value.toJsonObject()) + else -> hasField(key, value.toString()) + } + } +} + +// TODO RUMM-2949 Share forgeries/test configurations between modules +internal fun Iterable<*>.toJsonArray(): JsonElement { + val array = JsonArray() + forEach { + array.add(JsonSerializer.toJsonElement(it)) + } + return array +} + +internal fun Map<*, *>.toJsonObject(): JsonElement { + val obj = JsonObject() + forEach { + obj.add(it.key.toString(), JsonSerializer.toJsonElement(it.value)) + } + return obj +} + +internal fun JSONArray.toJsonArray(): JsonElement { + val obj = JsonArray() + for (index in 0 until length()) { + @Suppress("UnsafeThirdPartyFunctionCall") // iteration over indexes which exist + obj.add(JsonSerializer.toJsonElement(get(index))) + } + return obj +} + +internal fun JSONObject.toJsonObject(): JsonElement { + val obj = JsonObject() + for (key in keys()) { + @Suppress("UnsafeThirdPartyFunctionCall") // iteration over keys which exist + obj.add(key, JsonSerializer.toJsonElement(get(key))) + } + return obj +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/assertj/LogEventAssert.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/assertj/LogEventAssert.kt new file mode 100644 index 0000000000..3d07435740 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/assertj/LogEventAssert.kt @@ -0,0 +1,278 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.assertj + +import com.datadog.android.api.context.AccountInfo +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.log.internal.domain.DatadogLogGenerator +import com.datadog.android.log.model.LogEvent +import org.assertj.core.api.AbstractObjectAssert +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +internal class LogEventAssert(actual: LogEvent) : + AbstractObjectAssert(actual, LogEventAssert::class.java) { + + fun hasStatus(expected: LogEvent.Status): LogEventAssert { + assertThat(actual.status) + .overridingErrorMessage( + "Expected log to have level $expected but was ${actual.status}" + ) + .isEqualTo(expected) + return this + } + + fun hasServiceName(expected: String): LogEventAssert { + assertThat(actual.service) + .overridingErrorMessage( + "Expected log to have name $expected but was ${actual.service}" + ) + .isEqualTo(expected) + return this + } + + fun hasMessage(expected: String): LogEventAssert { + assertThat(actual.message) + .overridingErrorMessage( + "Expected log to have message $expected but was ${actual.message}" + ) + .isEqualTo(expected) + return this + } + + fun hasError(expected: LogEvent.Error): LogEventAssert { + assertThat(actual.error) + .overridingErrorMessage( + "Expected log to have error info $expected but was ${actual.error}" + ) + .isEqualTo(expected) + return this + } + + fun hasDate(expected: String): LogEventAssert { + assertThat(actual.date) + .overridingErrorMessage( + "Expected log to have date $expected but was ${actual.date}" + ) + .isEqualTo(expected) + return this + } + + fun hasDateAround(expected: Long): LogEventAssert { + val parsedDate = dateFormatter.parse(actual.date)?.time + assertThat(parsedDate) + .overridingErrorMessage( + "Expected log to have date around $expected but was $parsedDate" + ) + .isCloseTo(expected, Offset.offset(200L)) + return this + } + + fun hasExactlyAttributes(attributes: Map): LogEventAssert { + assertThat(actual.additionalProperties) + .hasSameSizeAs(attributes) + .containsAllEntriesOf(attributes) + return this + } + + fun hasExactlyTags(expected: Collection): LogEventAssert { + val serializedTags = expected.joinToString(separator = ",") + assertThat(actual.ddtags) + .overridingErrorMessage( + "Expected log to have tags $serializedTags but was ${actual.ddtags}" + ) + .isEqualTo(serializedTags) + return this + } + + fun hasLoggerName(expected: String): LogEventAssert { + assertThat(actual.logger.name) + .overridingErrorMessage( + "Expected log to have loggerName $expected but was ${actual.logger.name}" + ) + .isEqualTo(expected) + return this + } + + fun hasDeviceArchitecture(expected: String): LogEventAssert { + assertThat(actual.device.architecture) + .overridingErrorMessage( + "Expected device to have architecture $expected but was ${actual.device.architecture}" + ) + .isEqualTo(expected) + return this + } + + fun hasThreadName(expected: String): LogEventAssert { + assertThat(actual.logger.threadName) + .overridingErrorMessage( + "Expected log to have threadName $expected but was ${actual.logger.threadName}" + ) + .isEqualTo(expected) + return this + } + + fun hasLoggerVersion(expected: String): LogEventAssert { + assertThat(actual.logger.version) + .overridingErrorMessage( + "Expected log to have version $expected but was ${actual.logger.version}" + ) + .isEqualTo(expected) + + return this + } + + fun hasNetworkInfo(networkInfo: NetworkInfo): LogEventAssert { + assertThat(actual.network?.client?.connectivity) + .overridingErrorMessage( + "Expected LogEvent to have connectivity: " + + "${networkInfo.connectivity} but " + + "instead was: ${actual.network?.client?.connectivity}" + ) + .isEqualTo(networkInfo.connectivity.toString()) + assertThat(actual.network?.client?.downlinkKbps) + .overridingErrorMessage( + "Expected LogEvent to have downlinkKbps: " + + "${networkInfo.downKbps?.toString()} but " + + "instead was: ${actual.network?.client?.downlinkKbps}" + ) + .isEqualTo(networkInfo.downKbps?.toString()) + assertThat(actual.network?.client?.uplinkKbps) + .overridingErrorMessage( + "Expected LogEvent to have uplinkKbps: " + + "${networkInfo.upKbps?.toString()} but " + + "instead was: ${actual.network?.client?.uplinkKbps}" + ) + .isEqualTo(networkInfo.upKbps?.toString()) + assertThat(actual.network?.client?.signalStrength) + .overridingErrorMessage( + "Expected LogEvent to have signal strength: " + + "${networkInfo.strength?.toString()} but " + + "instead was: ${actual.network?.client?.signalStrength}" + ) + .isEqualTo(networkInfo.strength?.toString()) + assertThat(actual.network?.client?.simCarrier?.id) + .overridingErrorMessage( + "Expected LogEvent to have carrier id: " + + "${networkInfo.carrierId?.toString()} but " + + "instead was: ${actual.network?.client?.simCarrier?.id}" + ) + .isEqualTo(networkInfo.carrierId?.toString()) + assertThat(actual.network?.client?.simCarrier?.name) + .overridingErrorMessage( + "Expected LogEvent to have carrier name: " + + "${networkInfo.carrierName} but " + + "instead was: ${actual.network?.client?.simCarrier?.name}" + ) + .isEqualTo(networkInfo.carrierName) + return this + } + + fun doesNotHaveError(): LogEventAssert { + assertThat(actual.error) + .overridingErrorMessage( + "Expected log to not have a error info " + + "but instead it had ${actual.error}" + ) + .isNull() + return this + } + + fun doesNotHaveNetworkInfo(): LogEventAssert { + assertThat(actual.network) + .overridingErrorMessage( + "Expected log to not have a network info " + + "but instead it had ${actual.network}" + ) + .isNull() + return this + } + + fun hasUserInfo(userInfo: UserInfo): LogEventAssert { + assertThat(actual.usr?.name) + .overridingErrorMessage( + "Expected LogEvent to have user name: " + + "${userInfo.name} but " + + "instead was: ${actual.usr?.name}" + ) + .isEqualTo(userInfo.name) + assertThat(actual.usr?.email) + .overridingErrorMessage( + "Expected LogEvent to have user email: " + + "${userInfo.email} but " + + "instead was: ${actual.usr?.email}" + ) + .isEqualTo(userInfo.email) + assertThat(actual.usr?.id) + .overridingErrorMessage( + "Expected LogEvent to have user id: " + + "${userInfo.id} but " + + "instead was: ${actual.usr?.id}" + ) + .isEqualTo(userInfo.id) + assertThat(actual.usr?.additionalProperties) + .hasSameSizeAs(userInfo.additionalProperties) + .containsAllEntriesOf(userInfo.additionalProperties) + return this + } + + fun hasAccountInfo(accountInfo: AccountInfo?): LogEventAssert { + assertThat(actual.account?.name) + .overridingErrorMessage( + "Expected LogEvent to have account name: " + + "${accountInfo?.name} but " + + "instead was: ${actual.account?.name}" + ) + .isEqualTo(accountInfo?.name) + assertThat(actual.account?.id) + .overridingErrorMessage( + "Expected LogEvent to have account id: " + + "${accountInfo?.id} but " + + "instead was: ${actual.account?.id}" + ) + .isEqualTo(accountInfo?.id) + assertThat(actual.account?.additionalProperties) + .hasSameSizeAs(accountInfo?.extraInfo) + .containsAllEntriesOf(accountInfo?.extraInfo) + return this + } + + fun doesNotHaveUserInfo(): LogEventAssert { + assertThat(actual.usr) + .overridingErrorMessage( + "Expected log to not have an user info " + + "but instead it had ${actual.usr}" + ) + .isNull() + return this + } + + fun hasBuildId(buildId: String?): LogEventAssert { + assertThat(actual.buildId) + .overridingErrorMessage( + "Expected LogEvent to have build ID: $buildId" + + " but instead was ${actual.buildId}" + ) + .isEqualTo(buildId) + return this + } + + companion object { + + private val dateFormatter = + SimpleDateFormat(DatadogLogGenerator.ISO_8601, Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + + internal fun assertThat(actual: LogEvent): LogEventAssert = + LogEventAssert(actual) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/LogsFeatureTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/LogsFeatureTest.kt new file mode 100644 index 0000000000..0dfec2fcec --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/LogsFeatureTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal + +import android.content.Context +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.event.EventMapper +import com.datadog.android.event.MapperSerializer +import com.datadog.android.internal.utils.NULL_MAP_VALUE +import com.datadog.android.log.LogAttributes +import com.datadog.android.log.internal.domain.event.LogEventMapperWrapper +import com.datadog.android.log.internal.net.LogsRequestFactory +import com.datadog.android.log.internal.storage.LogsDataWriter +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.extension.toIsoFormattedTimestamp +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Locale +import java.util.UUID +import com.datadog.android.log.assertj.LogEventAssert.Companion.assertThat as assertThatLog + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class LogsFeatureTest { + + private lateinit var testedFeature: LogsFeature + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + @Mock + lateinit var mockLogsFeatureScope: FeatureScope + + @Mock + lateinit var mockEventWriteScope: EventWriteScope + + @Mock + lateinit var mockEventBatchWriter: EventBatchWriter + + @Mock + lateinit var mockDataWriter: DataWriter + + @Mock + lateinit var mockEventMapper: EventMapper + + @Mock + lateinit var mockApplicationContext: Context + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @Forgery + lateinit var fakeRumApplicationId: UUID + + @Forgery + lateinit var fakeRumSessionId: UUID + + @Forgery + lateinit var fakeRumViewId: UUID + + @Forgery + lateinit var fakeRumActionId: UUID + + @StringForgery(regex = "https://[a-z]+\\.com") + lateinit var fakeEndpointUrl: String + + @StringForgery(StringForgeryType.HEXADECIMAL) + lateinit var fakeSpanId: String + + @StringForgery(StringForgeryType.HEXADECIMAL) + lateinit var fakeTraceId: String + + @StringForgery(StringForgeryType.ALPHABETICAL) + lateinit var fakeThreadName: String + + @StringForgery(regex = "[a-z]{2,4}(\\.[a-z]{3,8}){2,4}") + lateinit var fakePackageName: String + + private var fakeServerTimeOffset: Long = 0L + + @BeforeEach + fun `set up`( + forge: Forge + ) { + val now = System.currentTimeMillis() + fakeServerTimeOffset = forge.aLong(min = -now, max = Long.MAX_VALUE - now) + + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + + whenever(mockApplicationContext.packageName) doReturn fakePackageName + whenever( + mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME) + ) doReturn mockLogsFeatureScope + + whenever(mockEventWriteScope.invoke(any())) doAnswer { + val callback = it.getArgument<(EventBatchWriter) -> Unit>(0) + callback.invoke(mockEventBatchWriter) + } + whenever(mockLogsFeatureScope.withWriteContext(any(), any())) doAnswer { + val callback = it.getArgument<(DatadogContext, EventWriteScope) -> Unit>(it.arguments.lastIndex) + callback.invoke(fakeDatadogContext, mockEventWriteScope) + } + + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerTimeOffset + ), + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + put( + Feature.RUM_FEATURE_NAME, + mapOf( + "application_id" to fakeRumApplicationId, + "session_id" to fakeRumSessionId, + "view_id" to fakeRumViewId, + "action_id" to fakeRumActionId + ) + ) + put( + Feature.TRACING_FEATURE_NAME, + mapOf( + "context@$fakeThreadName" to mapOf( + "span_id" to fakeSpanId, + "trace_id" to fakeTraceId + ) + ) + ) + } + ) + + testedFeature = LogsFeature(mockSdkCore, fakeEndpointUrl, mockEventMapper) + } + + @Test + fun `M initialize data writer W initialize()`() { + // When + testedFeature.onInitialize(mockApplicationContext) + + // Then + assertThat(testedFeature.dataWriter) + .isInstanceOf(LogsDataWriter::class.java) + } + + @Test + fun `M use the eventMapper W initialize()`() { + // When + testedFeature.onInitialize(mockApplicationContext) + + // Then + val dataWriter = testedFeature.dataWriter as? LogsDataWriter + val logMapperSerializer = dataWriter?.serializer as? MapperSerializer + val logEventMapperWrapper = logMapperSerializer + ?.getFieldValue>("eventMapper") + val logEventMapper = logEventMapperWrapper?.wrappedEventMapper + assertThat(logEventMapper).isSameAs(mockEventMapper) + } + + @Test + fun `M initialize packageName W initialize()`() { + // When + testedFeature.onInitialize(mockApplicationContext) + + // Then + assertThat(testedFeature.packageName).isEqualTo(fakePackageName) + } + + @Test + fun `M provide logs feature name W name()`() { + // When+Then + assertThat(testedFeature.name) + .isEqualTo(Feature.LOGS_FEATURE_NAME) + } + + @Test + fun `M provide logs request factory W requestFactory()`() { + // When+Then + assertThat(testedFeature.requestFactory) + .isInstanceOf(LogsRequestFactory::class.java) + } + + @Test + fun `M provide default storage configuration W storageConfiguration()`() { + // When+Then + assertThat(testedFeature.storageConfiguration) + .isEqualTo(FeatureStorageConfiguration.DEFAULT) + } + + @Test + fun `M add attributes W addAttribute`( + @StringForgery key: String, + @StringForgery value: String + ) { + // When + testedFeature.addAttribute(key, value) + + // Then + val attributes = testedFeature.getAttributes() + assertThat(attributes).containsEntry(key, value) + } + + @Test + fun `M remove attributes W removeAttribute`( + @StringForgery key: String, + @StringForgery value: String + ) { + // Given + testedFeature.addAttribute(key, value) + + // When + testedFeature.removeAttribute(key) + + // Then + val attributes = testedFeature.getAttributes() + assertThat(attributes).isEmpty() + } + + @Test + fun `M provide attribute snapshot W getAttributes`( + @StringForgery key: String, + @StringForgery value: String, + @StringForgery secondValue: String + ) { + // Given + testedFeature.addAttribute(key, value) + val attributes = testedFeature.getAttributes() + + // When + testedFeature.addAttribute(key, secondValue) + + // Then + assertThat(attributes).containsEntry(key, value) + } + + @Test + fun `M add attributes replaces null W addAttribute { null value }`( + @StringForgery key: String + ) { + testedFeature.addAttribute(key, null) + + // Then + assertThat(testedFeature.getAttributes()).containsEntry(key, NULL_MAP_VALUE) + } + + // region FeatureEventReceiver#onReceive + unknown + + @Test + fun `M log warning and do nothing W onReceive() { unknown event type }`() { + // Given + testedFeature.dataWriter = mockDataWriter + + // When + testedFeature.onReceive(Any()) + + // Then + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(firstValue()).isEqualTo( + LogsFeature.UNSUPPORTED_EVENT_TYPE.format( + Locale.US, + Any()::class.java.canonicalName + ) + ) + } + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockDataWriter) + } + + @Test + fun `M log warning and do nothing W onReceive() { unknown type property value }`( + forge: Forge + ) { + // Given + testedFeature.dataWriter = mockDataWriter + val event = mapOf( + "type" to forge.anAlphabeticalString() + ) + + // When + testedFeature.onReceive(event) + + // Then + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(firstValue()).isEqualTo( + LogsFeature.UNKNOWN_EVENT_TYPE_PROPERTY_VALUE.format(Locale.US, event["type"]) + ) + } + + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockDataWriter) + } + + // endregion + + // region FeatureEventReceiver#onReceive + span log event + + @ParameterizedTest + @EnumSource(ValueMissingType::class) + fun `M log warning and do nothing W onReceive() { corrupted mandatory fields, span log }`( + missingType: ValueMissingType, + @LongForgery fakeTimestamp: Long, + @StringForgery fakeMessage: String, + @StringForgery fakeLoggerName: String, + forge: Forge + ) { + // Given + testedFeature.dataWriter = mockDataWriter + val fakeAttributes = forge.exhaustiveAttributes() + val event = mutableMapOf( + "type" to "span_log", + "timestamp" to fakeTimestamp, + "message" to fakeMessage, + "loggerName" to fakeLoggerName, + "attributes" to fakeAttributes + ) + + when (missingType) { + ValueMissingType.MISSING -> event.remove( + forge.anElementFrom(event.keys.filterNot { it == "type" }) + ) + + ValueMissingType.NULL -> event[ + forge.anElementFrom(event.keys.filterNot { it == "type" }) + ] = null + + ValueMissingType.WRONG_TYPE -> event[ + forge.anElementFrom(event.keys.filterNot { it == "type" }) + ] = Any() + } + + // When + testedFeature.onReceive(event) + + // Then + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(firstValue()).isEqualTo( + LogsFeature.SPAN_LOG_EVENT_MISSING_MANDATORY_FIELDS_WARNING + ) + } + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockDataWriter) + } + + @Test + fun `M write span log event W onReceive() { span log }`( + @LongForgery fakeTimestamp: Long, + @StringForgery fakeMessage: String, + @StringForgery fakeLoggerName: String, + forge: Forge + ) { + // Given + testedFeature.dataWriter = mockDataWriter + val fakeAttributes = forge.exhaustiveAttributes() + val event = mutableMapOf( + "type" to "span_log", + "timestamp" to fakeTimestamp, + "message" to fakeMessage, + "loggerName" to fakeLoggerName, + "attributes" to fakeAttributes + ) + + // When + testedFeature.onReceive(event) + + // Then + verify(mockLogsFeatureScope).withWriteContext(eq(setOf(Feature.RUM_FEATURE_NAME)), any()) + argumentCaptor { + verify(mockDataWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + val log = lastValue + + assertThatLog(log) + .hasStatus(LogEvent.Status.TRACE) + .hasLoggerName(fakeLoggerName) + .hasServiceName(fakeDatadogContext.service) + .hasMessage(fakeMessage) + .hasThreadName(Thread.currentThread().name) + .hasDate((fakeTimestamp + fakeServerTimeOffset).toIsoFormattedTimestamp()) + .hasNetworkInfo(fakeDatadogContext.networkInfo) + .hasUserInfo(fakeDatadogContext.userInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + } + } + + // endregion + + enum class ValueMissingType { + MISSING, + NULL, + WRONG_TYPE + } + + inline fun R?.getFieldValue( + fieldName: String, + enclosingClass: Class? = this?.javaClass + ): T? { + if (this == null || enclosingClass == null) return null + val field = enclosingClass.getDeclaredField(fieldName) + field.isAccessible = true + return field.get(this) as T + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/DatadogLogGeneratorTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/DatadogLogGeneratorTest.kt new file mode 100644 index 0000000000..1bb443e537 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/DatadogLogGeneratorTest.kt @@ -0,0 +1,1264 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.domain + +import com.datadog.android.api.context.AccountInfo +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.api.feature.Feature +import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.log.LogAttributes +import com.datadog.android.log.assertj.LogEventAssert.Companion.assertThat +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.extension.asLogStatus +import com.datadog.android.utils.extension.toIsoFormattedTimestamp +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.util.UUID + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogLogGeneratorTest { + + lateinit var testedLogGenerator: DatadogLogGenerator + + lateinit var fakeServiceName: String + lateinit var fakeLoggerName: String + lateinit var fakeAttributes: Map + lateinit var fakeTags: Set + lateinit var fakeLogMessage: String + lateinit var fakeThrowable: Throwable + + @StringForgery(StringForgeryType.HEXADECIMAL) + lateinit var fakeSpanId: String + + @StringForgery(StringForgeryType.HEXADECIMAL) + lateinit var fakeTraceId: String + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @Forgery + lateinit var fakeRumApplicationId: UUID + + @Forgery + lateinit var fakeRumSessionId: UUID + + @Forgery + lateinit var fakeRumViewId: UUID + + @Forgery + lateinit var fakeRumActionId: UUID + + var fakeTimestamp = 0L + var fakeLevel: Int = 0 + lateinit var fakeThreadName: String + private var fakeTimeOffset: Long = 0L + + @BeforeEach + fun `set up`(forge: Forge) { + fakeServiceName = forge.anAlphabeticalString() + fakeLoggerName = forge.anAlphabeticalString() + fakeLogMessage = forge.anAlphabeticalString() + fakeLevel = forge.anInt(2, 8) + fakeAttributes = forge.aMap { anAlphabeticalString() to anInt() } + fakeTags = forge.aList { anAlphabeticalString() }.toSet() + fakeThrowable = forge.aThrowable() + fakeTimestamp = System.currentTimeMillis() + fakeThreadName = forge.anAlphabeticalString() + fakeTimeOffset = forge.aLong( + min = -fakeTimestamp, + max = Long.MAX_VALUE - fakeTimestamp + ) + + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeTimeOffset + ), + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + put( + Feature.RUM_FEATURE_NAME, + mapOf( + "application_id" to fakeRumApplicationId, + "session_id" to fakeRumSessionId, + "view_id" to fakeRumViewId, + "action_id" to fakeRumActionId + ) + ) + put( + Feature.TRACING_FEATURE_NAME, + mapOf( + "context@$fakeThreadName" to mapOf( + "span_id" to fakeSpanId, + "trace_id" to fakeTraceId + ) + ) + ) + } + ) + + testedLogGenerator = DatadogLogGenerator( + fakeServiceName + ) + } + + @Test + fun `M add log message W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasMessage(fakeLogMessage) + } + + @Test + fun `M add the log level W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(fakeLevel.asLogStatus()) + } + + @Test + fun `M add the service name W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasServiceName(fakeServiceName) + } + + @Test + fun `M add the service name from Datadog context W creating the Log { no service name }`() { + // WHEN + testedLogGenerator = DatadogLogGenerator() + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasServiceName(fakeDatadogContext.service) + } + + @Test + fun `M add the logger name W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasLoggerName(fakeLoggerName) + } + + @Test + fun `M add the logger version W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasLoggerVersion(fakeDatadogContext.sdkVersion) + } + + @Test + fun `M add the thread name W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasThreadName(fakeThreadName) + } + + @Test + fun `M add build ID W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasBuildId(fakeDatadogContext.appBuildId) + } + + @Test + fun `M add the log timestamp and correct the server offset W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val expected = (fakeTimestamp + fakeTimeOffset) + .toIsoFormattedTimestamp() + assertThat(log).hasDate(expected) + } + + @Test + fun `M add the throwable W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + threads = null + ) + ) + } + + @Test + fun `M not add sourceType W creating the Log { source_type attribute not set }`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = null, + threads = null + ) + ) + } + + @Test + fun `M add sourceType W creating the Log { source_type attribute set }`() { + // WHEN + val modifiedAttributes = fakeAttributes.toMutableMap().apply { + put(LogAttributes.SOURCE_TYPE, "fake_source_type") + } + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable.javaClass.canonicalName, + fakeThrowable.message, + fakeThrowable.stackTraceToString(), + modifiedAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = "fake_source_type", + threads = null + ) + ) + assertThat(log.additionalProperties).doesNotContainKey(LogAttributes.SOURCE_TYPE) + } + + @Test + fun `M not add fingerprint W creating the Log { fingerprint attribute not set }`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = null, + fingerprint = null, + threads = null + ) + ) + } + + @Test + fun `M add fingerprint W creating the Log { fingerprint attribute set }`( + @StringForgery fakeFingerprint: String + ) { + // WHEN + val modifiedAttributes = fakeAttributes.toMutableMap().apply { + put(LogAttributes.ERROR_FINGERPRINT, fakeFingerprint) + } + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + modifiedAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = null, + fingerprint = fakeFingerprint, + threads = null + ) + ) + assertThat(log.additionalProperties).doesNotContainKey(LogAttributes.ERROR_FINGERPRINT) + } + + @Test + fun `M add fingerprint W creating the Log { expanded error, fingerprint attribute set }`() { + // WHEN + val modifiedAttributes = fakeAttributes.toMutableMap().apply { + put(LogAttributes.ERROR_FINGERPRINT, "fake_fingerprint") + } + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable.javaClass.canonicalName, + fakeThrowable.message, + fakeThrowable.stackTraceToString(), + modifiedAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = null, + fingerprint = "fake_fingerprint", + threads = null + ) + ) + assertThat(log.additionalProperties).doesNotContainKey(LogAttributes.ERROR_FINGERPRINT) + } + + @Test + fun `M add the thread dump W creating the Log`( + @Forgery fakeThreads: List + ) { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName, + threads = fakeThreads + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + threads = fakeThreads.map { + LogEvent.Thread( + name = it.name, + crashed = it.crashed, + state = it.state, + stack = it.stack + ) + }.ifEmpty { null } + ) + ) + } + + @Test + fun `M use the Throwable class simple name W creating the Log { Throwable anonymous }`() { + // WHEN + val fakeAnonymousThrowable = object : Throwable(cause = fakeThrowable) {} + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeAnonymousThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeAnonymousThrowable.javaClass.simpleName, + stack = fakeAnonymousThrowable.stackTraceToString(), + message = fakeAnonymousThrowable.message, + threads = null + ) + ) + } + + @Test + fun `M use custom userInfo W creating the Log { userInfo provided }`( + @Forgery fakeCustomUserInfo: UserInfo + ) { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName, + userInfo = fakeCustomUserInfo + ) + + // THEN + assertThat(log).hasUserInfo(fakeCustomUserInfo) + } + + @Test + fun `M add the userInfo W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasUserInfo(fakeDatadogContext.userInfo) + } + + @Test + fun `M use custom accountInfo W creating the Log { accountInfo provided }`( + @Forgery fakeCustomAccountInfo: AccountInfo + ) { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName, + accountInfo = fakeCustomAccountInfo + ) + + // THEN + assertThat(log).hasAccountInfo(fakeCustomAccountInfo) + } + + @Test + fun `M add the accountInfo W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasAccountInfo(fakeDatadogContext.accountInfo) + } + + @Test + fun `M add the networkInfo W creating the Log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasNetworkInfo(fakeDatadogContext.networkInfo) + } + + @Test + fun `M not add the networkInfo W creating Log {networkInfoProvider is null}`() { + // GIVEN + testedLogGenerator = DatadogLogGenerator( + fakeServiceName + ) + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = false, + fakeLoggerName + ) + + // THEN + assertThat(log).doesNotHaveNetworkInfo() + } + + @Test + fun `M use custom networkInfo W creating Log { networkInfo provided }`( + @Forgery fakeCustomNetworkInfo: NetworkInfo + ) { + // GIVEN + testedLogGenerator = DatadogLogGenerator( + fakeServiceName + ) + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = false, + fakeLoggerName, + networkInfo = fakeCustomNetworkInfo + ) + + // THEN + assertThat(log).hasNetworkInfo(fakeCustomNetworkInfo) + } + + @Test + fun `M add the envNameTag W not empty`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val deserializedTags = log.ddtags.split(",") + Assertions.assertThat(deserializedTags) + .contains("${LogAttributes.ENV}:${fakeDatadogContext.env}") + } + + @Test + fun `M not add the envNameTag W empty`() { + // GIVEN + fakeDatadogContext = fakeDatadogContext.copy( + env = "" + ) + + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val expectedTags = fakeTags + + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}" + + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + assertThat(log).hasExactlyTags(expectedTags) + } + + @Test + fun `M add the appVersionTag W not empty`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val deserializedTags = log.ddtags.split(",") + Assertions.assertThat(deserializedTags) + .contains("${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}") + } + + @Test + fun `M not add the appVersionTag W empty`() { + // GIVEN + fakeDatadogContext = fakeDatadogContext.copy( + version = "" + ) + + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val expectedTags = fakeTags + + "${LogAttributes.ENV}:${fakeDatadogContext.env}" + + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + assertThat(log).hasExactlyTags(expectedTags) + } + + @Test + fun `M add the variantTag W not empty`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val deserializedTags = log.ddtags.split(",") + Assertions.assertThat(deserializedTags) + .contains("${LogAttributes.VARIANT}:${fakeDatadogContext.variant}") + } + + @Test + fun `M not add the variantTag W empty`() { + // GIVEN + fakeDatadogContext = fakeDatadogContext.copy( + variant = "" + ) + + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val expectedTags = fakeTags + + "${LogAttributes.ENV}:${fakeDatadogContext.env}" + + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}" + assertThat(log).hasExactlyTags(expectedTags) + } + + @Test + fun `M add architecture W created a log`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasDeviceArchitecture(fakeDatadogContext.deviceInfo.architecture) + } + + @Test + fun `M bundle the trace information W required`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + Assertions.assertThat(log.additionalProperties).containsAllEntriesOf( + mapOf( + LogAttributes.DD_TRACE_ID to fakeTraceId, + LogAttributes.DD_SPAN_ID to fakeSpanId + ) + ) + } + + @Test + fun `M do nothing W required to bundle the trace information {no active Span for given thread}`( + @StringForgery fakeOtherThreadName: String, + @StringForgery(StringForgeryType.HEXADECIMAL) fakeOtherThreadSpanId: String, + @StringForgery(StringForgeryType.HEXADECIMAL) fakeOtherThreadTraceId: String + ) { + // GIVEN + fakeDatadogContext = fakeDatadogContext.copy( + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + put( + Feature.TRACING_FEATURE_NAME, + mapOf( + "context@$fakeOtherThreadName" to mapOf( + "span_id" to fakeOtherThreadSpanId, + "trace_id" to fakeOtherThreadTraceId + ) + ) + ) + } + ) + + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val expectedAttributes = fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + assertThat(log).hasExactlyAttributes(expectedAttributes) + } + + @Test + fun `M do nothing W required to bundle the trace information {no tracing feature context}`() { + // GIVEN + fakeDatadogContext = fakeDatadogContext.copy( + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + remove(Feature.TRACING_FEATURE_NAME) + } + ) + + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val expectedAttributes = fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + assertThat(log).hasExactlyAttributes(expectedAttributes) + } + + @Test + fun `M do nothing W not required to bundle the trace information`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName, + bundleWithTraces = false + ) + + // THEN + val expectedAttributes = fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + assertThat(log).hasExactlyAttributes(expectedAttributes) + } + + @Test + fun `M bundle the RUM information W required`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + Assertions.assertThat(log.additionalProperties).containsAllEntriesOf( + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + } + + @Test + fun `M do nothing W required to bundle the rum information {no RUM context}`() { + // GIVEN + fakeDatadogContext = fakeDatadogContext.copy( + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + remove(Feature.RUM_FEATURE_NAME) + } + ) + + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + val expectedAttributes = fakeAttributes + mapOf( + LogAttributes.DD_TRACE_ID to fakeTraceId, + LogAttributes.DD_SPAN_ID to fakeSpanId + ) + assertThat(log).hasExactlyAttributes(expectedAttributes) + } + + @Test + fun `M do nothing W not required to bundle the rum information`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName, + bundleWithRum = false + ) + + // THEN + val expectedAttributes = fakeAttributes + mapOf( + LogAttributes.DD_TRACE_ID to fakeTraceId, + LogAttributes.DD_SPAN_ID to fakeSpanId + ) + assertThat(log).hasExactlyAttributes(expectedAttributes) + } + + @Test + fun `M use status CRITICAL W creating the Log { level ASSERT }`() { + // WHEN + val log = testedLogGenerator.generateLog( + android.util.Log.ASSERT, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.CRITICAL) + } + + @Test + fun `M use status ERROR W creating the Log { level ERROR }`() { + // WHEN + val log = testedLogGenerator.generateLog( + android.util.Log.ERROR, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.ERROR) + } + + @Test + fun `M use status EMERGENCY W creating the Log { level CRASH }`() { + // WHEN + val log = testedLogGenerator.generateLog( + DatadogLogGenerator.CRASH, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.EMERGENCY) + } + + @Test + fun `M use status WARN W creating the Log { level WARN }`() { + // WHEN + val log = testedLogGenerator.generateLog( + android.util.Log.WARN, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.WARN) + } + + @Test + fun `M use status INFO W creating the Log { level INFO }`() { + // WHEN + val log = testedLogGenerator.generateLog( + android.util.Log.INFO, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.INFO) + } + + @Test + fun `M use status DEBUG W creating the Log { level DEBUG }`() { + // WHEN + val log = testedLogGenerator.generateLog( + android.util.Log.DEBUG, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.DEBUG) + } + + @Test + fun `M use status TRACE W creating the Log { level VERBOSE }`() { + // WHEN + val log = testedLogGenerator.generateLog( + android.util.Log.VERBOSE, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.TRACE) + } + + @Test + fun `M use status DEBUG W creating the Log { other level }`(forge: Forge) { + // WHEN + val log = testedLogGenerator.generateLog( + forge.anInt(min = DatadogLogGenerator.CRASH + 1), + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasStatus(LogEvent.Status.DEBUG) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/event/LogEventMapperWrapperTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/event/LogEventMapperWrapperTest.kt new file mode 100644 index 0000000000..65cb84ab1c --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/event/LogEventMapperWrapperTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.domain.event + +import com.datadog.android.api.InternalLogger +import com.datadog.android.event.EventMapper +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.STRICT_STUBS) +@ForgeConfiguration(Configurator::class) +internal class LogEventMapperWrapperTest { + + lateinit var testedEventMapper: LogEventMapperWrapper + + @Mock + lateinit var mockWrappedEventMapper: EventMapper + + @Mock + lateinit var mockLogEvent: LogEvent + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedEventMapper = LogEventMapperWrapper(mockWrappedEventMapper, mockInternalLogger) + } + + @Test + fun `M map and return the LogEvent W map`() { + // GIVEN + whenever(mockWrappedEventMapper.map(mockLogEvent)).thenReturn(mockLogEvent) + + // WHEN + val mappedEvent = testedEventMapper.map(mockLogEvent) + + // THEN + verify(mockWrappedEventMapper).map(mockLogEvent) + Assertions.assertThat(mappedEvent).isEqualTo(mockLogEvent) + } + + @Test + fun `M return null if the mapped returned event is not the same instance W map`() { + // GIVEN + whenever(mockWrappedEventMapper.map(mockLogEvent)).thenReturn(mock()) + + // WHEN + val mappedEvent = testedEventMapper.map(mockLogEvent) + + // THEN + verify(mockWrappedEventMapper).map(mockLogEvent) + Assertions.assertThat(mappedEvent).isNull() + } + + @Test + fun `M log a warning if the mapped returned event is not the same instance W map`() { + // GIVEN + whenever(mockWrappedEventMapper.map(mockLogEvent)).thenReturn(mock()) + + // WHEN + testedEventMapper.map(mockLogEvent) + + // THEN + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.ERROR), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(firstValue()).isEqualTo( + LogEventMapperWrapper.NOT_SAME_EVENT_INSTANCE_WARNING_MESSAGE.format( + Locale.US, + mockLogEvent.toString() + ) + ) + } + } + + @Test + fun `M return null if the mapped returned event is null W map`() { + // GIVEN + whenever(mockWrappedEventMapper.map(mockLogEvent)).thenReturn(null) + + // WHEN + val mappedEvent = testedEventMapper.map(mockLogEvent) + + // THEN + verify(mockWrappedEventMapper).map(mockLogEvent) + Assertions.assertThat(mappedEvent).isNull() + } + + @Test + fun `M log a warning if the mapped returned event is null W map`() { + // GIVEN + whenever(mockWrappedEventMapper.map(mockLogEvent)).thenReturn(null) + + // WHEN + testedEventMapper.map(mockLogEvent) + + // THEN + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.INFO), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(firstValue()).isEqualTo( + LogEventMapperWrapper.EVENT_NULL_WARNING_MESSAGE.format( + Locale.US, + mockLogEvent.toString() + ) + ) + } + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/event/LogEventSerializerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/event/LogEventSerializerTest.kt new file mode 100644 index 0000000000..3d5623b1d7 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/event/LogEventSerializerTest.kt @@ -0,0 +1,564 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.domain.event + +import com.datadog.android.api.InternalLogger +import com.datadog.android.log.assertj.containsExtraAttributes +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.assertj.JsonObjectAssert +import com.datadog.tools.unit.assertj.JsonObjectAssert.Companion.assertThat +import com.datadog.tools.unit.forge.anException +import com.google.gson.JsonParser +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.STRICT_STUBS) +@ForgeConfiguration(Configurator::class) +internal class LogEventSerializerTest { + + private lateinit var testedSerializer: LogEventSerializer + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun `set up`() { + testedSerializer = LogEventSerializer(mockInternalLogger) + } + + @RepeatedTest(4) + fun `serializes full log as json`(@Forgery fakeLog: LogEvent) { + val serialized = testedSerializer.serialize(fakeLog) + assertSerializedLogMatchesInputLog(serialized, fakeLog) + } + + @Test + fun `ignores reserved attributes`(@Forgery fakeLog: LogEvent, forge: Forge) { + // Given + val logWithoutAttributes = fakeLog.copy(additionalProperties = mutableMapOf()) + val attributes = forge.aMap { + anElementFrom(LogEvent.RESERVED_PROPERTIES.asList()) to forge.anAsciiString() + }.toMutableMap() + val logWithReservedAttributes = fakeLog.copy(additionalProperties = attributes) + + // When + val serialized = testedSerializer.serialize(logWithReservedAttributes) + + // Then + assertSerializedLogMatchesInputLog(serialized, logWithoutAttributes) + } + + @Test + fun `ignores reserved tags keys`(@Forgery fakeLog: LogEvent, forge: Forge) { + // Given + val logWithoutTags = fakeLog.copy(ddtags = "") + val key = forge.anElementFrom("host", "device", "source", "service") + val value = forge.aNumericalString() + val reservedTag = "$key:$value" + val logWithReservedTags = fakeLog.copy(ddtags = reservedTag) + + // When + val serialized = testedSerializer.serialize(logWithReservedTags) + + // Then + assertSerializedLogMatchesInputLog(serialized, logWithoutTags) + } + + @Test + fun `M sanitise the user extra info keys W level deeper than 8`( + @Forgery fakeLog: LogEvent, + forge: Forge + ) { + // GIVEN + // we generate the bad key with depth level = 9 as the `usr` prefix will add 1 extra depth + val fakeBadKey = + forge.aList(size = 10) { forge.anAlphabeticalString() }.joinToString(".") + val lastDotIndex = fakeBadKey.lastIndexOf('.') + val expectedSanitisedKey = + fakeBadKey.replaceRange(lastDotIndex..lastDotIndex, "_") + val attributeValue = forge.anAlphabeticalString() + val fakeUserInfo = LogEvent.Usr( + additionalProperties = mutableMapOf( + fakeBadKey to attributeValue + ) + ) + + // WHEN + val newFakeLog = fakeLog.copy(usr = fakeUserInfo) + val serializedEvent = testedSerializer.serialize(newFakeLog) + val jsonObject = JsonParser.parseString(serializedEvent).asJsonObject + + // THEN + assertThat(jsonObject) + .hasField(KEY_USR) { + hasField( + expectedSanitisedKey, + attributeValue + ) + doesNotHaveField(fakeBadKey) + } + } + + @Test + fun `M sanitise the account extra info keys W level deeper than 8`( + @Forgery fakeLog: LogEvent, + forge: Forge + ) { + // GIVEN + // we generate the bad key with depth level = 9 as the `account` prefix will add 1 extra depth + val fakeBadKey = + forge.aList(size = 10) { forge.anAlphabeticalString() }.joinToString(".") + val lastDotIndex = fakeBadKey.lastIndexOf('.') + val expectedSanitisedKey = + fakeBadKey.replaceRange(lastDotIndex..lastDotIndex, "_") + val attributeValue = forge.anAlphabeticalString() + val fakeAccountInfo = LogEvent.Account( + additionalProperties = mutableMapOf( + fakeBadKey to attributeValue + ) + ) + + // WHEN + val newFakeLog = fakeLog.copy(account = fakeAccountInfo) + val serializedEvent = testedSerializer.serialize(newFakeLog) + val jsonObject = JsonParser.parseString(serializedEvent).asJsonObject + + // THEN + assertThat(jsonObject) + .hasField(KEY_ACCOUNT) { + hasField( + expectedSanitisedKey, + attributeValue + ) + doesNotHaveField(fakeBadKey) + } + } + + @Test + fun `M not throw W serialize() { usr#additionalProperties serialization throws }`( + @Forgery fakeLog: LogEvent, + forge: Forge + ) { + // Given + val faultyKey = forge.anAlphabeticalString() + val faultyObject = object { + override fun toString(): String { + throw forge.anException() + } + } + val faultyLogEvent = fakeLog.copy( + usr = fakeLog.usr?.copy( + additionalProperties = fakeLog.usr?.additionalProperties + ?.toMutableMap() + ?.apply { put(faultyKey, faultyObject) } + .orEmpty() + .toMutableMap() + ) + ) + + // When + val serialized = testedSerializer.serialize(faultyLogEvent) + + // Then + assertSerializedLogMatchesInputLog(serialized, fakeLog) + } + + @Test + fun `M not throw W serialize() { account#additionalProperties serialization throws }`( + @Forgery fakeLog: LogEvent, + forge: Forge + ) { + // Given + val faultyKey = forge.anAlphabeticalString() + val faultyObject = object { + override fun toString(): String { + throw forge.anException() + } + } + val faultyLogEvent = fakeLog.copy( + account = fakeLog.account?.copy( + additionalProperties = fakeLog.account?.additionalProperties + ?.toMutableMap() + ?.apply { put(faultyKey, faultyObject) } + .orEmpty() + .toMutableMap() + ) + ) + + // When + val serialized = testedSerializer.serialize(faultyLogEvent) + + // Then + assertSerializedLogMatchesInputLog(serialized, fakeLog) + } + + @Test + fun `M not throw W serialize() { additionalProperties serialization throws }`( + @Forgery fakeLog: LogEvent, + forge: Forge + ) { + // Given + val faultyKey = forge.anAlphabeticalString() + val faultyObject = object { + override fun toString(): String { + throw forge.anException() + } + } + val faultyLogEvent = fakeLog.copy( + additionalProperties = fakeLog.additionalProperties + .toMutableMap() + .apply { put(faultyKey, faultyObject) } + ) + + // When + val serialized = testedSerializer.serialize(faultyLogEvent) + + // Then + assertSerializedLogMatchesInputLog(serialized, fakeLog) + } + + // region Internal + + private fun assertSerializedLogMatchesInputLog( + serializedObject: String, + log: LogEvent + ) { + val jsonObject = JsonParser.parseString(serializedObject).asJsonObject + assertThat(jsonObject) + .hasField(KEY_MESSAGE, log.message) + .hasField(KEY_SERVICE_NAME, log.service) + .hasField(KEY_DATE, log.date) + .hasField(KEY_TAGS, log.ddtags) + log.status.let { + assertThat(jsonObject).hasField(KEY_STATUS, it.toJson()) + } + log.logger.let { + assertThat(jsonObject).hasField(KEY_LOGGER) { + hasLoggerInfo(it) + } + } + log.network?.let { + assertThat(jsonObject).hasField(KEY_NETWORK) { + hasField(KEY_CLIENT) { + hasNetworkInfo(it) + } + } + } + log.usr?.let { + assertThat(jsonObject).hasField(KEY_USR) { + hasUserInfo(it) + } + } + log.account?.let { + assertThat(jsonObject).hasField(KEY_ACCOUNT) { + hasAccountInfo(it) + } + } + log.error?.let { + assertThat(jsonObject).hasField(KEY_ERROR) { + hasErrorInfo(it) + } + } + assertThat(jsonObject).hasField(KEY_OS) { + hasOsInfo(log.os) + } + assertThat(jsonObject).hasField(KEY_DEVICE) { + hasDeviceInfo(log.device) + } + } + + private fun JsonObjectAssert.hasNetworkInfo( + network: LogEvent.Network + ) { + hasField( + KEY_NETWORK_CONNECTIVITY, + network.client.connectivity + ) + val simCarrier = network.client.simCarrier + if (simCarrier != null) { + hasField(KEY_SIM_CARRIER) { + val simCarrierName = simCarrier.name + if (simCarrierName != null) { + hasField(KEY_NETWORK_CARRIER_NAME, simCarrierName) + } else { + doesNotHaveField(KEY_NETWORK_CARRIER_NAME) + } + val simCarrierId = simCarrier.id + if (simCarrierId != null) { + hasField( + KEY_NETWORK_CARRIER_ID, + simCarrierId + ) + } else { + doesNotHaveField(KEY_NETWORK_CARRIER_ID) + } + } + } else { + doesNotHaveField(KEY_SIM_CARRIER) + } + val uplinkKbps = network.client.uplinkKbps + if (uplinkKbps != null) { + hasField(KEY_NETWORK_UP_KBPS, uplinkKbps) + } else { + doesNotHaveField(KEY_NETWORK_UP_KBPS) + } + val downlinkKbps = network.client.downlinkKbps + if (downlinkKbps != null) { + hasField( + KEY_NETWORK_DOWN_KBPS, + downlinkKbps + ) + } else { + doesNotHaveField(KEY_NETWORK_DOWN_KBPS) + } + val signalStrength = network.client.signalStrength + if (signalStrength != null) { + hasField( + KEY_NETWORK_SIGNAL_STRENGTH, + signalStrength + ) + } else { + doesNotHaveField(KEY_NETWORK_SIGNAL_STRENGTH) + } + } + + private fun JsonObjectAssert.hasUserInfo( + userInfo: LogEvent.Usr + ) { + val userName = userInfo.name + val userEmail = userInfo.email + val userId = userInfo.id + if (userId != null) { + hasField(KEY_USR_ID, userId) + } else { + doesNotHaveField(KEY_USR_ID) + } + if (userName != null) { + hasField(KEY_USR_NAME, userName) + } else { + doesNotHaveField(KEY_USR_NAME) + } + if (userEmail != null) { + hasField(KEY_USR_EMAIL, userEmail) + } else { + doesNotHaveField(KEY_USR_EMAIL) + } + containsExtraAttributes( + userInfo.additionalProperties.minus(LogEvent.Usr.RESERVED_PROPERTIES) + ) + } + + private fun JsonObjectAssert.hasAccountInfo( + accountInfo: LogEvent.Account + ) { + val accountName = accountInfo.name + val accountId = accountInfo.id + if (accountId != null) { + hasField(KEY_ACCOUNT_ID, accountId) + } else { + doesNotHaveField(KEY_ACCOUNT_ID) + } + if (accountName != null) { + hasField(KEY_ACCOUNT_NAME, accountName) + } else { + doesNotHaveField(KEY_ACCOUNT_NAME) + } + containsExtraAttributes( + accountInfo.additionalProperties.minus(LogEvent.Account.RESERVED_PROPERTIES) + ) + } + + private fun JsonObjectAssert.hasLoggerInfo(loggerInfo: LogEvent.Logger) { + val loggerName = loggerInfo.name + val threadName = loggerInfo.threadName + val sdkVersion = loggerInfo.version + hasField(KEY_NAME, loggerName) + if (threadName != null) { + hasField(KEY_THREAD_NAME, threadName) + } else { + doesNotHaveField(KEY_THREAD_NAME) + } + hasNullableField(KEY_VERSION, sdkVersion) + } + + private fun JsonObjectAssert.hasErrorInfo(errorInfo: LogEvent.Error) { + val errorMessage = errorInfo.message + val errorKind = errorInfo.kind + val errorStack = errorInfo.stack + val errorSourceType = errorInfo.sourceType + if (errorMessage != null) { + hasField(KEY_MESSAGE, errorMessage) + } else { + doesNotHaveField(KEY_MESSAGE) + } + if (errorKind != null) { + hasField(KEY_KIND, errorKind) + } else { + doesNotHaveField(KEY_KIND) + } + if (errorStack != null) { + hasField(KEY_STACK, errorStack) + } else { + doesNotHaveField(KEY_STACK) + } + if (errorSourceType != null) { + hasField(KEY_SOURCE_TYPE, errorSourceType) + } else { + doesNotHaveField(KEY_SOURCE_TYPE) + } + } + + private fun JsonObjectAssert.hasOsInfo(osInfo: LogEvent.Os) { + val osName = osInfo.name + val osBuild = osInfo.build + val osVersion = osInfo.version + val osVersionMajor = osInfo.versionMajor + hasField(KEY_NAME, osName) + if (osBuild != null) { + hasField(KEY_BUILD, osBuild) + } else { + doesNotHaveField(KEY_BUILD) + } + hasField(KEY_VERSION, osVersion) + hasField(KEY_VERSION_MAJOR, osVersionMajor) + } + + private fun JsonObjectAssert.hasDeviceInfo(deviceInfo: LogEvent.LogEventDevice) { + val deviceType = deviceInfo.type + val deviceName = deviceInfo.name + val deviceModel = deviceInfo.model + val deviceBrand = deviceInfo.brand + val deviceArhitecture = deviceInfo.architecture + val deviceLocale = deviceInfo.locale + val deviceLocales = deviceInfo.locales + val deviceTimezone = deviceInfo.timeZone + val deviceBatteryLevel = deviceInfo.batteryLevel + val devicePowerSavingMode = deviceInfo.powerSavingMode + val deviceBrightnessLevel = deviceInfo.brightnessLevel + if (deviceType != null) { + hasField(KEY_TYPE, deviceType.name.lowercase(Locale.US)) + } else { + doesNotHaveField(KEY_TYPE) + } + if (deviceName != null) { + hasField(KEY_NAME, deviceName) + } else { + doesNotHaveField(KEY_NAME) + } + if (deviceModel != null) { + hasField(KEY_MODEL, deviceModel) + } else { + doesNotHaveField(KEY_MODEL) + } + if (deviceBrand != null) { + hasField(KEY_BRAND, deviceBrand) + } else { + doesNotHaveField(KEY_BRAND) + } + if (deviceArhitecture != null) { + hasField(KEY_ARCHITECTURE, deviceArhitecture) + } else { + doesNotHaveField(KEY_ARCHITECTURE) + } + if (deviceLocale != null) { + hasField(KEY_LOCALE, deviceLocale) + } else { + doesNotHaveField(KEY_LOCALE) + } + if (deviceLocales != null) { + hasField(KEY_LOCALES, deviceLocales) + } else { + doesNotHaveField(KEY_LOCALES) + } + if (deviceTimezone != null) { + hasField(KEY_TIME_ZONE, deviceTimezone) + } else { + doesNotHaveField(KEY_TIME_ZONE) + } + if (deviceBatteryLevel != null) { + hasField(KEY_BATTERY_LEVEL, deviceBatteryLevel) + } else { + doesNotHaveField(KEY_BATTERY_LEVEL) + } + if (devicePowerSavingMode != null) { + hasField(KEY_POWER_SAVING_MODE, devicePowerSavingMode) + } else { + doesNotHaveField(KEY_POWER_SAVING_MODE) + } + if (deviceBrightnessLevel != null) { + hasField(KEY_BRIGHTNESS_LEVEL, deviceBrightnessLevel) + } else { + doesNotHaveField(KEY_BRIGHTNESS_LEVEL) + } + } + + // endregion + + companion object { + + private const val KEY_SERVICE_NAME = "service" + private const val KEY_NAME = "name" + private const val KEY_DATE = "date" + private const val KEY_MESSAGE = "message" + private const val KEY_KIND = "kind" + private const val KEY_STACK = "stack" + private const val KEY_SOURCE_TYPE = "source_type" + private const val KEY_ERROR = "error" + private const val KEY_VERSION = "version" + private const val KEY_THREAD_NAME = "thread_name" + private const val KEY_STATUS = "status" + private const val KEY_TAGS = "ddtags" + private const val KEY_SIM_CARRIER = "sim_carrier" + private const val KEY_NETWORK_CARRIER_ID: String = "id" + private const val KEY_NETWORK_CARRIER_NAME: String = "name" + private const val KEY_NETWORK_CONNECTIVITY: String = "connectivity" + private const val KEY_NETWORK_DOWN_KBPS: String = "downlink_kbps" + private const val KEY_NETWORK_SIGNAL_STRENGTH: String = "signal_strength" + private const val KEY_NETWORK_UP_KBPS: String = "uplink_kbps" + private const val KEY_USR = "usr" + private const val KEY_ACCOUNT = "account" + private const val KEY_NETWORK = "network" + private const val KEY_CLIENT = "client" + private const val KEY_USR_NAME = "name" + private const val KEY_USR_EMAIL = "email" + private const val KEY_USR_ID = "id" + private const val KEY_ACCOUNT_NAME = "name" + private const val KEY_ACCOUNT_ID = "id" + private const val KEY_LOGGER = "logger" + private const val KEY_BUILD = "build" + private const val KEY_VERSION_MAJOR = "version_major" + private const val KEY_OS = "os" + private const val KEY_DEVICE = "device" + private const val KEY_TYPE = "type" + private const val KEY_MODEL = "model" + private const val KEY_BRAND = "brand" + private const val KEY_ARCHITECTURE = "architecture" + private const val KEY_LOCALE = "locale" + private const val KEY_LOCALES = "locales" + private const val KEY_TIME_ZONE = "time_zone" + private const val KEY_BATTERY_LEVEL = "battery_level" + private const val KEY_POWER_SAVING_MODE = "power_saving_mode" + private const val KEY_BRIGHTNESS_LEVEL = "brightness_level" + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt new file mode 100644 index 0000000000..91f4f4f312 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +import com.datadog.android.tests.elmyr.exhaustiveAttributes +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class CombinedLogHandlerTest { + + lateinit var testedHandler: LogHandler + + lateinit var mockDelegateLogHandlers: Array + + @StringForgery + lateinit var fakeMessage: String + + @StringForgery(StringForgeryType.ALPHABETICAL) + lateinit var fakeTags: Set + + lateinit var fakeAttributes: Map + + @IntForgery(min = 2, max = 8) + var fakeLevel: Int = 0 + + @Forgery + lateinit var fakeThrowable: Throwable + + @StringForgery + lateinit var fakeErrorKind: String + + @StringForgery + lateinit var fakeErrorMessage: String + + @StringForgery + lateinit var fakeErrorStackTrace: String + + @BeforeEach + fun `set up`(forge: Forge) { + mockDelegateLogHandlers = forge.aList { mock() }.toTypedArray() + fakeAttributes = forge.exhaustiveAttributes() + + testedHandler = CombinedLogHandler(*mockDelegateLogHandlers) + } + + @Test + fun `M forward log to all delegates W handleLog {throwable}`() { + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + mockDelegateLogHandlers.forEach { + verify(it).handleLog(fakeLevel, fakeMessage, fakeThrowable, fakeAttributes, fakeTags) + } + } + + @Test + fun `M forward log to all delegates W handleLog {throwable, background thread}`( + @StringForgery(StringForgeryType.ALPHABETICAL) threadName: String + ) { + // Given + val countDownLatch = CountDownLatch(1) + val thread = Thread( + { + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + countDownLatch.countDown() + }, + threadName + ) + + // When + thread.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // Then + mockDelegateLogHandlers.forEach { + verify(it).handleLog(fakeLevel, fakeMessage, fakeThrowable, fakeAttributes, fakeTags) + } + } + + @Test + fun `M forward log to all delegates W handleLog {null throwable}`() { + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + + // Then + mockDelegateLogHandlers.forEach { + verify(it).handleLog(fakeLevel, fakeMessage, null, emptyMap(), emptySet()) + } + } + + @Test + fun `M forward log to all delegates W handleLog {stacktrace}`() { + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + + // Then + mockDelegateLogHandlers.forEach { + verify(it).handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + } + } + + @Test + fun `M forward log to all delegates W handleLog {stacktrace, background thread}`( + @StringForgery(StringForgeryType.ALPHABETICAL) threadName: String + ) { + // Given + val countDownLatch = CountDownLatch(1) + val thread = Thread( + { + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + countDownLatch.countDown() + }, + threadName + ) + + // When + thread.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // Then + mockDelegateLogHandlers.forEach { + verify(it).handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + } + } + + @Test + fun `M forward log to all delegates W handleLog {null stacktrace}`() { + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + null, + null, + emptyMap(), + emptySet() + ) + + // Then + mockDelegateLogHandlers.forEach { + verify(it).handleLog(fakeLevel, fakeMessage, null, null, null, emptyMap(), emptySet()) + } + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt new file mode 100644 index 0000000000..7b46450f89 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt @@ -0,0 +1,391 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +import com.datadog.android.tests.elmyr.exhaustiveAttributes +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ConditionalLogHandlerTest { + + lateinit var testedHandler: LogHandler + + @Mock + lateinit var mockDelegateLogHandler: LogHandler + + @StringForgery + lateinit var fakeMessage: String + + @StringForgery(StringForgeryType.ALPHABETICAL) + lateinit var fakeTags: Set + + lateinit var fakeAttributes: Map + + @IntForgery(min = 2, max = 8) + var fakeLevel: Int = 0 + + var fakeCondition = false + + @Forgery + lateinit var fakeThrowable: Throwable + + @StringForgery + lateinit var fakeErrorKind: String + + @StringForgery + lateinit var fakeErrorMessage: String + + @StringForgery + lateinit var fakeErrorStackTrace: String + + @BeforeEach + fun `set up`(forge: Forge) { + fakeAttributes = forge.exhaustiveAttributes() + + testedHandler = ConditionalLogHandler(mockDelegateLogHandler) { _, _ -> + fakeCondition + } + } + + @Test + fun `M forward log W handleLog (throwable, condition true)`() { + // Given + fakeCondition = true + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockDelegateLogHandler).handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + } + + @Test + fun `M forward log on background thread W handleLog (throwable, condition true)`( + @StringForgery threadName: String + ) { + // Given + fakeCondition = true + val countDownLatch = CountDownLatch(1) + val thread = Thread( + { + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + countDownLatch.countDown() + }, + threadName + ) + + // When + thread.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // Then + verify(mockDelegateLogHandler).handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + } + + @Test + fun `M forward minimal log W handleLog (null throwable, condition true)`() { + // Given + fakeCondition = true + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + + // Then + verify(mockDelegateLogHandler).handleLog( + fakeLevel, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `M not forward log W handleLog (throwable, condition false)`() { + // Given + fakeCondition = false + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verifyNoInteractions(mockDelegateLogHandler) + } + + @Test + fun `M not forward log on background thread W handleLog (throwable, condition false)`( + @StringForgery threadName: String + ) { + // Given + fakeCondition = false + val countDownLatch = CountDownLatch(1) + val thread = Thread( + { + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + countDownLatch.countDown() + }, + threadName + ) + + // When + thread.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // Then + verifyNoInteractions(mockDelegateLogHandler) + } + + @Test + fun `M not forward minimal log W handleLog (null throwable, condition false)`() { + // Given + fakeCondition = false + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + + // Then + verifyNoInteractions(mockDelegateLogHandler) + } + + @Test + fun `M forward log W handleLog (stacktrace, condition true)`() { + // Given + fakeCondition = true + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockDelegateLogHandler).handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + } + + @Test + fun `M forward log on background thread W handleLog (stacktrace, condition true)`( + @StringForgery threadName: String + ) { + // Given + fakeCondition = true + val countDownLatch = CountDownLatch(1) + val thread = Thread( + { + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + countDownLatch.countDown() + }, + threadName + ) + + // When + thread.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // Then + verify(mockDelegateLogHandler).handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + } + + @Test + fun `M forward minimal log W handleLog (null stacktrace, condition true)`() { + // Given + fakeCondition = true + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + null, + null, + emptyMap(), + emptySet() + ) + + // Then + verify(mockDelegateLogHandler).handleLog( + fakeLevel, + fakeMessage, + null, + null, + null, + emptyMap(), + emptySet() + ) + } + + @Test + fun `M not forward log W handleLog (stacktrace, condition false)`() { + // Given + fakeCondition = false + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + + // Then + verifyNoInteractions(mockDelegateLogHandler) + } + + @Test + fun `M not forward log on background thread W handleLog (stacktrace, condition false)`( + @StringForgery threadName: String + ) { + // Given + fakeCondition = false + val countDownLatch = CountDownLatch(1) + val thread = Thread( + { + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeErrorKind, + fakeErrorMessage, + fakeErrorStackTrace, + fakeAttributes, + fakeTags + ) + countDownLatch.countDown() + }, + threadName + ) + + // When + thread.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // Then + verifyNoInteractions(mockDelegateLogHandler) + } + + @Test + fun `M not forward minimal log W handleLog (null stacktrace, condition false)`() { + // Given + fakeCondition = false + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + null, + null, + emptyMap(), + emptySet() + ) + + // Then + verifyNoInteractions(mockDelegateLogHandler) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandlerTest.kt new file mode 100644 index 0000000000..c91515e208 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/DatadogLogHandlerTest.kt @@ -0,0 +1,1112 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.core.sampling.Sampler +import com.datadog.android.log.LogAttributes +import com.datadog.android.log.assertj.LogEventAssert.Companion.assertThat +import com.datadog.android.log.internal.LogsFeature +import com.datadog.android.log.internal.domain.DatadogLogGenerator +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.extension.asLogStatus +import com.datadog.android.utils.extension.toIsoFormattedTimestamp +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.annotation.StringForgeryType +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import android.util.Log as AndroidLog + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogLogHandlerTest { + + private lateinit var testedHandler: LogHandler + + private lateinit var fakeServiceName: String + private lateinit var fakeLoggerName: String + private lateinit var fakeMessage: String + private lateinit var fakeTags: Set + private lateinit var fakeAttributes: Map + private var fakeLevel: Int = 0 + + @Forgery + lateinit var fakeThrowable: Throwable + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @Forgery + lateinit var fakeRumApplicationId: UUID + + @Forgery + lateinit var fakeRumSessionId: UUID + + @Forgery + lateinit var fakeRumViewId: UUID + + @Forgery + lateinit var fakeRumActionId: UUID + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + @Mock + lateinit var mockLogsFeatureScope: FeatureScope + + @Mock + lateinit var mockLogsFeature: LogsFeature + + @Mock + lateinit var mockRumFeature: FeatureScope + + @Mock + lateinit var mockEventWriteScope: EventWriteScope + + @Mock + lateinit var mockEventBatchWriter: EventBatchWriter + + @Mock + lateinit var mockWriter: DataWriter + + @Mock + lateinit var mockSampler: Sampler + + @BeforeEach + fun `set up`(forge: Forge) { + fakeServiceName = forge.anAlphabeticalString() + fakeLoggerName = forge.anAlphabeticalString() + fakeMessage = forge.anAlphabeticalString() + fakeLevel = forge.anInt(2, 8) + fakeAttributes = forge.aMap { anAlphabeticalString() to anInt() } + fakeTags = forge.aList { anAlphabeticalString() }.toSet() + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = 0L + ), + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + put( + Feature.RUM_FEATURE_NAME, + mapOf( + "application_id" to fakeRumApplicationId, + "session_id" to fakeRumSessionId, + "view_id" to fakeRumViewId, + "action_id" to fakeRumActionId + ) + ) + } + ) + + whenever( + mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME) + ) doReturn mockLogsFeatureScope + whenever(mockLogsFeatureScope.unwrap()) doReturn mockLogsFeature + whenever(mockEventWriteScope.invoke(any())) doAnswer { + val callback = it.getArgument<(EventBatchWriter) -> Unit>(0) + callback.invoke(mockEventBatchWriter) + } + whenever(mockLogsFeatureScope.withWriteContext(any(), any())) doAnswer { + val callback = it.getArgument<(DatadogContext, EventWriteScope) -> Unit>(it.arguments.lastIndex) + callback.invoke(fakeDatadogContext, mockEventWriteScope) + } + + whenever( + mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME) + ) doReturn mockRumFeature + + testedHandler = DatadogLogHandler( + loggerName = fakeLoggerName, + logGenerator = DatadogLogGenerator( + fakeServiceName + ), + sdkCore = mockSdkCore, + writer = mockWriter, + attachNetworkInfo = true + ) + } + + @Test + fun `forward log to LogWriter`() { + // Given + val now = System.currentTimeMillis() + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasThreadName(Thread.currentThread().name) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDateAround(now) + .hasNetworkInfo(fakeDatadogContext.networkInfo) + .hasUserInfo(fakeDatadogContext.userInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + fakeTags + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + .doesNotHaveError() + } + } + + @Test + fun `M not forward log to LogWriter W level is below the min supported`( + forge: Forge + ) { + // Given + testedHandler = DatadogLogHandler( + loggerName = fakeLoggerName, + logGenerator = DatadogLogGenerator( + fakeServiceName + ), + sdkCore = mockSdkCore, + writer = mockWriter, + attachNetworkInfo = true, + minLogPriority = forge.anInt(min = fakeLevel + 1) + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + forge.aNullable { fakeThrowable }, + fakeAttributes, + fakeTags + ) + + // Then + verifyNoInteractions(mockWriter, mockSampler) + } + + @Test + fun `forward log to LogWriter with throwable`() { + // Given + val now = System.currentTimeMillis() + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasThreadName(Thread.currentThread().name) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDateAround(now) + .hasNetworkInfo(fakeDatadogContext.networkInfo) + .hasUserInfo(fakeDatadogContext.userInfo) + .hasAccountInfo(fakeDatadogContext.accountInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + fakeTags + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + .hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + message = fakeThrowable.message, + stack = fakeThrowable.stackTraceToString(), + threads = null + ) + ) + } + } + + @Test + fun `forward log to LogWriter with error strings`( + @StringForgery errorKind: String, + @StringForgery errorMessage: String, + @StringForgery errorStack: String + ) { + // Given + val now = System.currentTimeMillis() + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + errorKind, + errorMessage, + errorStack, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor().apply { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasThreadName(Thread.currentThread().name) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDateAround(now) + .hasNetworkInfo(fakeDatadogContext.networkInfo) + .hasUserInfo(fakeDatadogContext.userInfo) + .hasAccountInfo(fakeDatadogContext.accountInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + fakeTags + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + .hasError( + LogEvent.Error( + kind = errorKind, + message = errorMessage, + stack = errorStack + ) + ) + } + } + + // region Forwarding to RUM + + @Test + fun `doesn't forward low level log to RumMonitor`(forge: Forge) { + // Given + fakeLevel = forge.anInt(AndroidLog.VERBOSE, AndroidLog.ERROR) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verifyNoInteractions(mockRumFeature) + } + + @ParameterizedTest + @ValueSource(ints = [AndroidLog.ERROR, AndroidLog.ASSERT]) + fun `forward error log to RumMonitor`(logLevel: Int) { + // When + testedHandler.handleLog( + logLevel, + fakeMessage, + null, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockRumFeature).sendEvent( + mapOf( + "type" to "logger_error", + "message" to fakeMessage, + "throwable" to null, + "attributes" to fakeAttributes + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [AndroidLog.ERROR, AndroidLog.ASSERT]) + fun `forward error log to RumMonitor with throwable`(logLevel: Int) { + // When + testedHandler.handleLog( + logLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockRumFeature).sendEvent( + mapOf( + "type" to "logger_error", + "message" to fakeMessage, + "throwable" to fakeThrowable, + "attributes" to fakeAttributes + ) + ) + } + + @Test + fun `doesn't forward low level log with string errors to RumMonitor`( + forge: Forge, + @StringForgery errorKind: String, + @StringForgery errorMessage: String, + @StringForgery errorStack: String + ) { + // Given + fakeLevel = forge.anInt(AndroidLog.VERBOSE, AndroidLog.ERROR) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + errorKind, + errorMessage, + errorStack, + fakeAttributes, + fakeTags + ) + + // Then + verifyNoInteractions(mockRumFeature) + } + + @ParameterizedTest + @ValueSource(ints = [AndroidLog.ERROR, AndroidLog.ASSERT]) + fun `forward error log with error strings to RumMonitor`( + logLevel: Int, + @StringForgery errorKind: String, + @StringForgery errorMessage: String, + @StringForgery errorStack: String + ) { + // When + testedHandler.handleLog( + logLevel, + fakeMessage, + errorKind, + errorMessage, + errorStack, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockRumFeature).sendEvent( + mapOf( + "type" to "logger_error_with_stacktrace", + "message" to fakeMessage, + "stacktrace" to errorStack, + "attributes" to fakeAttributes + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [AndroidLog.ERROR, AndroidLog.ASSERT]) + fun `forward error log with feature attributes to RumMonitor`( + logLevel: Int, + @StringForgery key: String, + @StringForgery value: String + ) { + // Given + whenever(mockLogsFeature.getAttributes()) doReturn mapOf( + key to value + ) + + // When + testedHandler.handleLog( + logLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + argumentCaptor> { + verify(mockRumFeature).sendEvent( + capture() + ) + @Suppress("UNCHECKED_CAST") + assertThat(lastValue["attributes"] as Map) + .containsEntry(key, value) + .containsAllEntriesOf(fakeAttributes) + } + } + + @ParameterizedTest + @ValueSource(ints = [AndroidLog.ERROR, AndroidLog.ASSERT]) + fun `forward error log with feature attributes and error strings to RumMonitor`( + logLevel: Int, + @StringForgery errorKind: String, + @StringForgery errorMessage: String, + @StringForgery errorStack: String, + @StringForgery key: String, + @StringForgery value: String + ) { + // Given + whenever(mockLogsFeature.getAttributes()) doReturn mapOf( + key to value + ) + + // When + testedHandler.handleLog( + logLevel, + fakeMessage, + errorKind, + errorMessage, + errorStack, + fakeAttributes, + fakeTags + ) + + // Then + argumentCaptor> { + verify(mockRumFeature).sendEvent( + capture() + ) + @Suppress("UNCHECKED_CAST") + assertThat(lastValue["attributes"] as Map) + .containsEntry(key, value) + .containsAllEntriesOf(fakeAttributes) + } + } + + // endregion + + @Test + fun `forward log with custom timestamp to LogWriter`(forge: Forge) { + // Given + val customTimestamp = forge.aPositiveLong() + val serverTimeOffsetMs = forge.aLong(min = -10000L, max = 10000L) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = serverTimeOffsetMs + ) + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + customTimestamp + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasThreadName(Thread.currentThread().name) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDate((customTimestamp + serverTimeOffsetMs).toIsoFormattedTimestamp()) + .hasNetworkInfo(fakeDatadogContext.networkInfo) + .hasUserInfo(fakeDatadogContext.userInfo) + .hasAccountInfo(fakeDatadogContext.accountInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + fakeTags + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + } + } + + @Test + fun `forward log to LogWriter on background thread`(forge: Forge) { + // Given + val now = System.currentTimeMillis() + val threadName = forge.anAlphabeticalString() + val countDownLatch = CountDownLatch(1) + + // When + val thread = Thread( + { + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + countDownLatch.countDown() + }, + threadName + ) + + thread.start() + countDownLatch.await(1, TimeUnit.SECONDS) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasThreadName(threadName) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDateAround(now) + .hasNetworkInfo(fakeDatadogContext.networkInfo) + .hasUserInfo(fakeDatadogContext.userInfo) + .hasAccountInfo(fakeDatadogContext.accountInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + fakeTags + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + } + } + + @Test + fun `forward log to LogWriter without network info`() { + // Given + val now = System.currentTimeMillis() + testedHandler = DatadogLogHandler( + loggerName = fakeLoggerName, + logGenerator = DatadogLogGenerator( + fakeServiceName + ), + sdkCore = mockSdkCore, + writer = mockWriter, + attachNetworkInfo = false + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasThreadName(Thread.currentThread().name) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDateAround(now) + .doesNotHaveNetworkInfo() + .hasUserInfo(fakeDatadogContext.userInfo) + .hasAccountInfo(fakeDatadogContext.accountInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + fakeTags + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + } + } + + @Test + fun `forward minimal log to LogWriter`() { + // Given + val now = System.currentTimeMillis() + fakeDatadogContext = fakeDatadogContext.copy( + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + remove(Feature.RUM_FEATURE_NAME) + } + ) + testedHandler = DatadogLogHandler( + loggerName = fakeLoggerName, + logGenerator = DatadogLogGenerator( + fakeServiceName + ), + sdkCore = mockSdkCore, + writer = mockWriter, + attachNetworkInfo = false + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + emptyMap(), + emptySet() + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasThreadName(Thread.currentThread().name) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDateAround(now) + .doesNotHaveNetworkInfo() + .hasUserInfo(fakeDatadogContext.userInfo) + .hasAccountInfo(fakeDatadogContext.accountInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes(emptyMap()) + .hasExactlyTags( + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + .doesNotHaveError() + } + } + + // region Attributes + + @Test + fun `M add feature attributes W handleLog`( + @StringForgery key: String, + @StringForgery value: String + ) { + // Given + whenever(mockLogsFeature.getAttributes()) doReturn mapOf( + key to value + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + emptyMap(), + fakeTags + ) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue.additionalProperties) + .containsEntry(key, value) + } + } + + @Test + fun `M combine feature attributes with logged attributes W handleLog`( + @StringForgery key: String, + @StringForgery value: String, + @StringForgery loggerKey: String, + @StringForgery loggerValue: String + ) { + // Given + whenever(mockLogsFeature.getAttributes()) doReturn mapOf( + key to value + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + mapOf(loggerKey to loggerValue), + fakeTags + ) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue.additionalProperties) + .containsEntry(key, value) + .containsEntry(loggerKey, loggerValue) + } + } + + @Test + fun `M overwrite attributes with logged attributes W handleLog`( + @StringForgery key: String, + @StringForgery value: String, + @StringForgery loggerValue: String + ) { + // Given + whenever(mockLogsFeature.getAttributes()) doReturn mapOf( + key to value + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + mapOf(key to loggerValue), + fakeTags + ) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue.additionalProperties) + .containsEntry(key, loggerValue) + } + } + + // endregion + + @Test + fun `it will add the span id and trace id if we active an active tracer`( + @StringForgery(type = StringForgeryType.HEXADECIMAL) fakeSpanId: String, + @StringForgery(type = StringForgeryType.HEXADECIMAL) fakeTraceId: String + ) { + // Given + val threadName = Thread.currentThread().name + + val tracingContext = mapOf( + "context@$threadName" to mapOf( + "span_id" to fakeSpanId, + "trace_id" to fakeTraceId + ) + ) + fakeDatadogContext = fakeDatadogContext.copy( + featuresContext = fakeDatadogContext.featuresContext.toMutableMap().apply { + put(Feature.TRACING_FEATURE_NAME, tracingContext) + } + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue.additionalProperties) + .containsEntry(LogAttributes.DD_TRACE_ID, fakeTraceId) + .containsEntry(LogAttributes.DD_SPAN_ID, fakeSpanId) + } + } + + @Test + fun `it will not add trace deps if we do not have active an active tracer`() { + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue.additionalProperties) + .doesNotContainKey(LogAttributes.DD_TRACE_ID) + .doesNotContainKey(LogAttributes.DD_SPAN_ID) + } + } + + @Test + fun `it will add the Rum context`() { + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + null, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME, Feature.TRACING_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue.additionalProperties) + .containsEntry( + LogAttributes.RUM_APPLICATION_ID, + fakeRumApplicationId + ) + .containsEntry(LogAttributes.RUM_SESSION_ID, fakeRumSessionId) + .containsEntry(LogAttributes.RUM_VIEW_ID, fakeRumViewId) + .containsEntry(LogAttributes.RUM_ACTION_ID, fakeRumActionId) + } + } + + @Test + fun `it will not add trace deps if the flag was set to false`() { + // Given + testedHandler = DatadogLogHandler( + loggerName = fakeLoggerName, + logGenerator = DatadogLogGenerator( + fakeServiceName + ), + sdkCore = mockSdkCore, + writer = mockWriter, + attachNetworkInfo = true, + bundleWithTraces = false + ) + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verify(mockLogsFeatureScope).withWriteContext( + eq(setOf(Feature.RUM_FEATURE_NAME)), + any() + ) + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue.additionalProperties) + .doesNotContainKey(LogAttributes.DD_TRACE_ID) + .doesNotContainKey(LogAttributes.DD_SPAN_ID) + } + } + + @Test + fun `it will sample out the logs when required`() { + // Given + whenever(mockSampler.sample(Unit)).thenReturn(false) + testedHandler = DatadogLogHandler( + loggerName = fakeLoggerName, + logGenerator = DatadogLogGenerator( + fakeServiceName + ), + sdkCore = mockSdkCore, + writer = mockWriter, + attachNetworkInfo = true, + bundleWithTraces = false, + sampler = mockSampler + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + verifyNoInteractions(mockWriter) + } + + @Test + fun `it will sample in the logs when required`() { + // Given + val now = System.currentTimeMillis() + whenever(mockSampler.sample(Unit)).thenReturn(true) + testedHandler = DatadogLogHandler( + loggerName = fakeLoggerName, + logGenerator = DatadogLogGenerator( + fakeServiceName + ), + sdkCore = mockSdkCore, + writer = mockWriter, + attachNetworkInfo = true, + bundleWithTraces = false, + sampler = mockSampler + ) + + // When + testedHandler.handleLog( + fakeLevel, + fakeMessage, + fakeThrowable, + fakeAttributes, + fakeTags + ) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + + assertThat(lastValue) + .hasServiceName(fakeServiceName) + .hasLoggerName(fakeLoggerName) + .hasStatus(fakeLevel.asLogStatus()) + .hasMessage(fakeMessage) + .hasDateAround(now) + .hasNetworkInfo(fakeDatadogContext.networkInfo) + .hasUserInfo(fakeDatadogContext.userInfo) + .hasAccountInfo(fakeDatadogContext.accountInfo) + .hasBuildId(fakeDatadogContext.appBuildId) + .hasExactlyAttributes( + fakeAttributes + mapOf( + LogAttributes.RUM_APPLICATION_ID to fakeRumApplicationId, + LogAttributes.RUM_SESSION_ID to fakeRumSessionId, + LogAttributes.RUM_VIEW_ID to fakeRumViewId, + LogAttributes.RUM_ACTION_ID to fakeRumActionId + ) + ) + .hasExactlyTags( + fakeTags + setOf( + "${LogAttributes.ENV}:${fakeDatadogContext.env}", + "${LogAttributes.APPLICATION_VERSION}:${fakeDatadogContext.version}", + "${LogAttributes.VARIANT}:${fakeDatadogContext.variant}" + ) + ) + } + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandlerTest.kt new file mode 100644 index 0000000000..acdf61e5f5 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/LogcatLogHandlerTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger + +import fr.xgouchet.elmyr.Case +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.util.Locale + +@ExtendWith(ForgeExtension::class) +internal class LogcatLogHandlerTest { + + lateinit var testedHandler: LogcatLogHandler + + @StringForgery + lateinit var fakeServiceName: String + + @BeforeEach + fun `set up`() { + testedHandler = LogcatLogHandler(fakeServiceName, true) + } + + @Test + fun `resolves stack trace element null if in release mode`() { + testedHandler = LogcatLogHandler(fakeServiceName, true, isDebug = false) + + val element = testedHandler.getCallerStackElement() + + assertThat(element) + .isNull() + } + + @Test + fun `resolves stack trace element null if useClassnameAsTag=false`() { + testedHandler = LogcatLogHandler(fakeServiceName, false, isDebug = true) + + val element = testedHandler.getCallerStackElement() + + assertThat(element) + .isNull() + } + + @Test + fun `resolves stack trace element from caller`() { + testedHandler = LogcatLogHandler(fakeServiceName, true, isDebug = true) + + val element = testedHandler.getCallerStackElement() + + checkNotNull(element) + assertThat(element.className) + .isEqualTo(javaClass.canonicalName) + } + + @Test + fun `resolves nested stack trace element from caller`() { + testedHandler = LogcatLogHandler(fakeServiceName, true, isDebug = true) + + var element: StackTraceElement? = null + + // Don't convert it to lambda, because Kotlin won't create wrapper class, + // read more https://kotlinlang.org/docs/whatsnew15.html#sam-adapters-via-invokedynamic + @Suppress("ObjectLiteralToLambda") + val runnable = object : Runnable { + override fun run() { + element = testedHandler.getCallerStackElement() + } + } + runnable.run() + + assertThat(element!!.className) + .isEqualTo( + "${javaClass.canonicalName}" + + "\$resolves nested stack trace element from caller" + + "\$runnable\$1" + ) + } + + @RepeatedTest(4) + fun `resolves valid stack trace element when wrapped in timber`(forge: Forge) { + // Given + val forgeFileName: Forge.() -> String = { + "${this.anAlphabeticalString(Case.ANY)}.${this.aStringMatching("(kt|java)")}" + } + + val ignoredElements = forge.aList { + val className = if (aBool()) { + // generate from ignored class names pattern + LogcatLogHandler.IGNORED_CLASS_NAMES.random() + } else { + // generate from ignored packages prefixes pattern + val packagePrefix = LogcatLogHandler.IGNORED_PACKAGE_PREFIXES.random() + val packageSuffix = anAlphabeticalString(Case.ANY) + .replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() + } + "$packagePrefix.$packageSuffix" + } + + StackTraceElement( + className, + anAlphabeticalString(Case.ANY), + forgeFileName(this), + aSmallInt() + ) + } + + val validElements = forge.aList { + val className = "com.${anAlphabeticalString(Case.LOWER, 5)}" + + ".${anAlphabeticalString(Case.LOWER, 6)}" + + ".${anAlphabeticalString(Case.LOWER, 7)}" + + ".${anAlphabeticalString(Case.ANY, 8)}" + + StackTraceElement( + className, + anAlphabeticalString(Case.ANY), + forgeFileName(this), + aSmallInt() + ) + } + + val stackTrace = (ignoredElements + validElements).toTypedArray() + + // When + val element = testedHandler.findValidCallStackElement(stackTrace) + + // Then + checkNotNull(element) + assertThat(element.className) + .isEqualTo(validElements.first().className) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/net/LogsRequestFactoryTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/net/LogsRequestFactoryTest.kt new file mode 100644 index 0000000000..b9660fb137 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/net/LogsRequestFactoryTest.kt @@ -0,0 +1,136 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.net + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.net.RequestExecutionContext +import com.datadog.android.api.net.RequestFactory +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.internal.utils.join +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class LogsRequestFactoryTest { + + private lateinit var testedFactory: LogsRequestFactory + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @BeforeEach + fun `set up`() { + testedFactory = LogsRequestFactory( + customEndpointUrl = null, + internalLogger = InternalLogger.UNBOUND + ) + } + + @Suppress("NAME_SHADOWING") + @Test + fun `M create a proper request W create()`( + @Forgery batchData: List, + @Forgery executionContext: RequestExecutionContext, + @StringForgery batchMetadata: String, + forge: Forge + ) { + // Given + val batchMetadata = forge.aNullable { batchMetadata.toByteArray() } + + // When + val request = testedFactory.create(fakeDatadogContext, executionContext, batchData, batchMetadata) + + // Then + requireNotNull(request) + assertThat(request.url).isEqualTo( + "${fakeDatadogContext.site.intakeEndpoint}/api/v2/logs?" + + "ddsource=${fakeDatadogContext.source}" + ) + assertThat(request.contentType).isEqualTo(RequestFactory.CONTENT_TYPE_JSON) + assertThat(request.headers.minus(RequestFactory.HEADER_REQUEST_ID)).isEqualTo( + mapOf( + RequestFactory.HEADER_API_KEY to fakeDatadogContext.clientToken, + RequestFactory.HEADER_EVP_ORIGIN to fakeDatadogContext.source, + RequestFactory.HEADER_EVP_ORIGIN_VERSION to fakeDatadogContext.sdkVersion + ) + ) + assertThat(request.headers[RequestFactory.HEADER_REQUEST_ID]).isNotEmpty() + assertThat(request.id).isEqualTo(request.headers[RequestFactory.HEADER_REQUEST_ID]) + assertThat(request.description).isEqualTo("Logs Request") + assertThat(request.body).isEqualTo( + batchData.map { it.data } + .join( + separator = ",".toByteArray(), + prefix = "[".toByteArray(), + suffix = "]".toByteArray(), + internalLogger = InternalLogger.UNBOUND + ) + ) + } + + @Suppress("NAME_SHADOWING") + @Test + fun `M create a proper request W create() { custom endpoint }`( + @StringForgery(regex = "https://[a-z]+\\.com(/[a-z]+)+") fakeEndpoint: String, + @Forgery batchData: List, + @StringForgery batchMetadata: String, + @Forgery executionContext: RequestExecutionContext, + forge: Forge + ) { + // Given + testedFactory = LogsRequestFactory( + customEndpointUrl = fakeEndpoint, + internalLogger = InternalLogger.UNBOUND + ) + val batchMetadata = forge.aNullable { batchMetadata.toByteArray() } + + // When + val request = testedFactory.create(fakeDatadogContext, executionContext, batchData, batchMetadata) + + // Then + requireNotNull(request) + assertThat(request.url).isEqualTo("$fakeEndpoint?ddsource=${fakeDatadogContext.source}") + assertThat(request.contentType).isEqualTo(RequestFactory.CONTENT_TYPE_JSON) + assertThat(request.headers.minus(RequestFactory.HEADER_REQUEST_ID)).isEqualTo( + mapOf( + RequestFactory.HEADER_API_KEY to fakeDatadogContext.clientToken, + RequestFactory.HEADER_EVP_ORIGIN to fakeDatadogContext.source, + RequestFactory.HEADER_EVP_ORIGIN_VERSION to fakeDatadogContext.sdkVersion + ) + ) + assertThat(request.headers[RequestFactory.HEADER_REQUEST_ID]).isNotEmpty() + assertThat(request.id).isEqualTo(request.headers[RequestFactory.HEADER_REQUEST_ID]) + assertThat(request.description).isEqualTo("Logs Request") + assertThat(request.body).isEqualTo( + batchData.map { it.data } + .join( + separator = ",".toByteArray(), + prefix = "[".toByteArray(), + suffix = "]".toByteArray(), + internalLogger = InternalLogger.UNBOUND + ) + ) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/storage/LogsDataWriterTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/storage/LogsDataWriterTest.kt new file mode 100644 index 0000000000..242ee0ccda --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/storage/LogsDataWriterTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.storage + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.api.storage.EventType +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.log.model.LogEvent +import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.aThrowable +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class LogsDataWriterTest { + + private lateinit var testedWriter: LogsDataWriter + + @Mock + lateinit var mockSerializer: Serializer + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockEventBatchWriter: EventBatchWriter + + @Forgery + lateinit var fakeEventType: EventType + + @BeforeEach + fun `set up`() { + testedWriter = LogsDataWriter(mockSerializer, mockInternalLogger) + } + + @Test + fun `M write data W write()`( + @Forgery fakeLogEvent: LogEvent + ) { + // Given + val fakeSerializedLogEvent = fakeLogEvent.toString() + whenever(mockSerializer.serialize(fakeLogEvent)) doReturn fakeSerializedLogEvent + whenever( + mockEventBatchWriter.write( + RawBatchEvent(data = fakeSerializedLogEvent.toByteArray()), + null, + fakeEventType + ) + ) doReturn true + + // When + val result = testedWriter.write(mockEventBatchWriter, fakeLogEvent, fakeEventType) + + // Then + assertThat(result).isTrue + verify(mockEventBatchWriter).write( + RawBatchEvent(data = fakeSerializedLogEvent.toByteArray()), + null, + fakeEventType + ) + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M return false W write() { bytes were not written }`( + @Forgery fakeLogEvent: LogEvent + ) { + // Given + val fakeSerializedLogEvent = fakeLogEvent.toString() + whenever(mockSerializer.serialize(fakeLogEvent)) doReturn fakeSerializedLogEvent + whenever( + mockEventBatchWriter.write( + RawBatchEvent(data = fakeSerializedLogEvent.toByteArray()), + null, + fakeEventType + ) + ) doReturn false + + // When + val result = testedWriter.write(mockEventBatchWriter, fakeLogEvent, fakeEventType) + + // Then + assertThat(result).isFalse + verify(mockEventBatchWriter).write( + RawBatchEvent(data = fakeLogEvent.toString().toByteArray()), + null, + fakeEventType + ) + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `M return false W write() { serialization returns null }`( + @Forgery fakeLogEvent: LogEvent + ) { + // Given + whenever(mockSerializer.serialize(fakeLogEvent)) doReturn null + + // When + val result = testedWriter.write(mockEventBatchWriter, fakeLogEvent, fakeEventType) + + // Then + assertThat(result).isFalse + + verifyNoInteractions(mockEventBatchWriter, mockInternalLogger) + } + + @Test + fun `M return false and log error W write() { serialization failed with exception }`( + @Forgery fakeLogEvent: LogEvent, + forge: Forge + ) { + // Given + val fakeThrowable = forge.aThrowable() + whenever(mockSerializer.serialize(fakeLogEvent)) doThrow fakeThrowable + + // When + val result = testedWriter.write(mockEventBatchWriter, fakeLogEvent, fakeEventType) + + // Then + assertThat(result).isFalse + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.ERROR), + eq(listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY)), + capture(), + eq(fakeThrowable), + eq(false), + eq(null) + ) + assertThat(firstValue()).isEqualTo("Error serializing LogEvent model") + } + + verifyNoInteractions(mockEventBatchWriter) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/utils/LogUtilsTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/utils/LogUtilsTest.kt new file mode 100644 index 0000000000..4d04b4104f --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/utils/LogUtilsTest.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.utils + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import java.util.TimeZone + +@Extensions( + ExtendWith(ForgeExtension::class) +) +@ForgeConfiguration(Configurator::class) +internal class LogUtilsTest { + + @Test + fun `M build a SimpleDateFormat with the ISO format W buildSimpleDateFormat()`() { + // When + val simpleDateFormat = buildLogDateFormat() + + // Then + assertThat(simpleDateFormat.toPattern()).isEqualTo(ISO_8601) + assertThat(simpleDateFormat.timeZone).isEqualTo(TimeZone.getTimeZone("UTC")) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/model/LogEventTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/model/LogEventTest.kt new file mode 100644 index 0000000000..81571d32ff --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/model/LogEventTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.model + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings +@ForgeConfiguration(Configurator::class) +internal class LogEventTest { + + @RepeatedTest(8) + fun `M serialize deserialized event W toJson()+fromJson()`( + @Forgery event: LogEvent + ) { + // Given + val json = event.toJson().toString() + + // When + val result = LogEvent.fromJson(json) + + // Then + assertThat(result).isEqualTo(result) + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/extension/IntExt.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/extension/IntExt.kt new file mode 100644 index 0000000000..36028ad511 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/extension/IntExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.extension + +import android.util.Log +import com.datadog.android.log.internal.domain.DatadogLogGenerator +import com.datadog.android.log.model.LogEvent + +fun Int.asLogStatus(): LogEvent.Status { + return when (this) { + Log.ASSERT -> LogEvent.Status.CRITICAL + Log.ERROR -> LogEvent.Status.ERROR + Log.WARN -> LogEvent.Status.WARN + Log.INFO -> LogEvent.Status.INFO + Log.DEBUG -> LogEvent.Status.DEBUG + Log.VERBOSE -> LogEvent.Status.TRACE + DatadogLogGenerator.CRASH -> LogEvent.Status.EMERGENCY + else -> LogEvent.Status.DEBUG + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/extension/LongExt.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/extension/LongExt.kt new file mode 100644 index 0000000000..f85a860e78 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/extension/LongExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.extension + +import com.datadog.android.log.internal.domain.DatadogLogGenerator +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +fun Long.toIsoFormattedTimestamp(iso: String = DatadogLogGenerator.ISO_8601): String { + val simpleDateFormat = SimpleDateFormat(iso, Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + return simpleDateFormat.format(this) +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/Configurator.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/Configurator.kt new file mode 100644 index 0000000000..88ab12478a --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/Configurator.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.tests.elmyr.useCoreFactories +import com.datadog.tools.unit.forge.BaseConfigurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.jvm.useJvmFactories + +internal class Configurator : BaseConfigurator() { + + override fun configure(forge: Forge) { + super.configure(forge) + + forge.useCoreFactories() + + forge.addFactory(LogEventForgeryFactory()) + forge.addFactory(LogsConfigurationForgeryFactory()) + + forge.useJvmFactories() + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/ForgeExt.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/ForgeExt.kt new file mode 100644 index 0000000000..3a8f649e15 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/ForgeExt.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.log.internal.utils.ISO_8601 +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.jvm.ext.aTimestamp +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +internal fun Forge.aFormattedTimestamp(format: String = ISO_8601): String { + val simpleDateFormat = SimpleDateFormat(format, Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + return simpleDateFormat.format(this.aTimestamp()) +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/LogEventForgeryFactory.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/LogEventForgeryFactory.kt new file mode 100644 index 0000000000..a7893c272b --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/LogEventForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.api.context.AccountInfo +import com.datadog.android.api.context.DeviceInfo +import com.datadog.android.api.context.DeviceType +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.api.context.UserInfo +import com.datadog.android.log.model.LogEvent +import com.datadog.tools.unit.forge.aThrowable +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import java.util.UUID + +internal class LogEventForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): LogEvent { + val networkInfo = forge.aNullable() + val userInfo = forge.aNullable() + val accountInfo = forge.aNullable() + val reservedKeysAsSet = mutableSetOf().apply { + LogEvent.RESERVED_PROPERTIES.forEach { + this.add(it) + } + } + val deviceInfo: DeviceInfo = forge.getForgery() + return LogEvent( + service = forge.anAlphabeticalString(), + status = forge.aValueFrom(LogEvent.Status::class.java), + message = forge.anAlphabeticalString(), + date = forge.aFormattedTimestamp(), + buildId = forge.aNullable { getForgery().toString() }, + error = forge.aNullable { + val throwable = forge.aNullable { aThrowable() } + LogEvent.Error( + message = throwable?.message, + stack = throwable?.stackTraceToString(), + kind = throwable?.javaClass?.canonicalName ?: throwable?.javaClass?.simpleName, + threads = aNullable { + aList { + LogEvent.Thread( + name = anAlphaNumericalString(), + crashed = aBool(), + stack = aThrowable().stackTraceToString(), + state = aNullable { getForgery().name.lowercase() } + ) + } + } + ) + }, + additionalProperties = forge.exhaustiveAttributes( + excludedKeys = reservedKeysAsSet, + filterThreshold = 0f + ), + ddtags = forge.exhaustiveTags().joinToString(separator = ","), + usr = forge.aNullable { + LogEvent.Usr( + id = userInfo?.id, + name = userInfo?.name, + email = userInfo?.email, + additionalProperties = userInfo?.additionalProperties + ?.toMutableMap() ?: mutableMapOf() + + ) + }, + account = accountInfo?.let { + LogEvent.Account( + id = it.id, + name = it.name, + additionalProperties = it.extraInfo.toMutableMap() + ) + }, + network = forge.aNullable { + LogEvent.Network( + client = LogEvent.Client( + simCarrier = forge.aNullable { + LogEvent.SimCarrier( + id = networkInfo?.carrierId?.toString(), + name = networkInfo?.carrierName + ) + }, + signalStrength = networkInfo?.strength?.toString(), + uplinkKbps = networkInfo?.upKbps?.toString(), + downlinkKbps = networkInfo?.downKbps?.toString(), + connectivity = networkInfo?.connectivity?.toString().orEmpty() + ) + ) + }, + logger = LogEvent.Logger( + name = forge.anAlphabeticalString(), + version = forge.aStringMatching("[0-9]\\.[0-9]\\.[0-9]"), + threadName = forge.aNullable { forge.anAlphabeticalString() } + ), + device = LogEvent.LogEventDevice( + type = resolveDeviceType(deviceInfo.deviceType), + name = deviceInfo.deviceName, + model = deviceInfo.deviceModel, + brand = deviceInfo.deviceBrand, + architecture = deviceInfo.architecture, + locale = forge.aNullable { anAlphabeticalString() }, + locales = forge.aNullable { aList { anAlphabeticalString() } }, + timeZone = forge.aNullable { anAlphabeticalString() }, + powerSavingMode = forge.aNullable { aBool() } + ), + dd = LogEvent.Dd( + device = LogEvent.DdDevice( + architecture = deviceInfo.architecture + ) + ), + os = LogEvent.Os( + name = deviceInfo.osName, + version = deviceInfo.osVersion, + versionMajor = deviceInfo.osMajorVersion, + build = forge.aNullable { anAlphabeticalString() } + ) + ) + } + + private fun resolveDeviceType(deviceType: DeviceType): LogEvent.Type { + return when (deviceType) { + DeviceType.MOBILE -> LogEvent.Type.MOBILE + DeviceType.TABLET -> LogEvent.Type.TABLET + DeviceType.TV -> LogEvent.Type.TV + DeviceType.DESKTOP -> LogEvent.Type.DESKTOP + else -> LogEvent.Type.OTHER + } + } + + private fun Forge.exhaustiveTags(): List { + return aList { aStringMatching("[a-z]([a-z0-9_:./-]{0,198}[a-z0-9_./-])?") } + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/LogsConfigurationForgeryFactory.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/LogsConfigurationForgeryFactory.kt new file mode 100644 index 0000000000..6c67216417 --- /dev/null +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/utils/forge/LogsConfigurationForgeryFactory.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils.forge + +import com.datadog.android.log.LogsConfiguration +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import org.mockito.kotlin.mock + +class LogsConfigurationForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): LogsConfiguration { + return LogsConfiguration( + customEndpointUrl = forge.aNullable { + aStringMatching("https://[a-z]+\\.com") + }, + eventMapper = mock() + ) + } +} diff --git a/dd-sdk-android-fresco/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/features/dd-sdk-android-logs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from dd-sdk-android-fresco/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to features/dd-sdk-android-logs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/features/dd-sdk-android-logs/src/testDebug/java/com/datadog/android/log/internal/logger/LogcatLogHandlerJavaTest.java b/features/dd-sdk-android-logs/src/testDebug/java/com/datadog/android/log/internal/logger/LogcatLogHandlerJavaTest.java new file mode 100644 index 0000000000..486117388e --- /dev/null +++ b/features/dd-sdk-android-logs/src/testDebug/java/com/datadog/android/log/internal/logger/LogcatLogHandlerJavaTest.java @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.log.internal.logger; + +import fr.xgouchet.elmyr.annotation.StringForgery; +import fr.xgouchet.elmyr.junit5.ForgeExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.atomic.AtomicReference; + +@SuppressWarnings("KotlinInternalInJava") +@ExtendWith(ForgeExtension.class) +public class LogcatLogHandlerJavaTest { + + LogcatLogHandler testedHandler; + + @StringForgery + String fakeServiceName; + + @Test + void resolves_stack_trace_element_null_if_in_release_mode() { + testedHandler = new LogcatLogHandler(fakeServiceName, true, false); + + StackTraceElement element = testedHandler.getCallerStackElement$dd_sdk_android_logs_debug(); + + assertThat(element) + .isNull(); + } + + @Test + void resolves_stack_trace_element_null_if_useClassnameAsTag_is_false() { + testedHandler = new LogcatLogHandler(fakeServiceName, false, true); + + StackTraceElement element = testedHandler.getCallerStackElement$dd_sdk_android_logs_debug(); + + assertThat(element) + .isNull(); + } + + @Test + void resolves_stack_trace_element_from_caller() { + testedHandler = new LogcatLogHandler(fakeServiceName, true, true); + + StackTraceElement element = testedHandler.getCallerStackElement$dd_sdk_android_logs_debug(); + + assertThat(element).isNotNull(); + assertThat(element.getClassName()) + .isEqualTo(getClass().getCanonicalName()); + } + + @Test + void resolves_nested_stack_trace_element_from_caller() { + testedHandler = new LogcatLogHandler(fakeServiceName, true, true); + + AtomicReference elementRef = new AtomicReference<>(); + + Runnable runnable = new Runnable() { + @Override + public void run() { + elementRef.set(testedHandler.getCallerStackElement$dd_sdk_android_logs_debug()); + } + }; + runnable.run(); + + assertThat(elementRef).isNotNull(); + assertThat(elementRef.get().getClassName()) + .isEqualTo(getClass().getCanonicalName() + "$1"); + } +} diff --git a/features/dd-sdk-android-logs/transitiveDependencies b/features/dd-sdk-android-logs/transitiveDependencies new file mode 100644 index 0000000000..b099002d83 --- /dev/null +++ b/features/dd-sdk-android-logs/transitiveDependencies @@ -0,0 +1,9 @@ +Dependencies List + +androidx.annotation:annotation-jvm:1.9.1 : 59 Kb +com.google.code.gson:gson:2.10.1 : 276 Kb +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 : 1706 Kb +org.jetbrains:annotations:13.0 : 17 Kb + +Total transitive dependencies size : 2 Mb + diff --git a/features/dd-sdk-android-ndk/CMakeLists.txt b/features/dd-sdk-android-ndk/CMakeLists.txt new file mode 100644 index 0000000000..ecd1ade32c --- /dev/null +++ b/features/dd-sdk-android-ndk/CMakeLists.txt @@ -0,0 +1,10 @@ +project("dd-sdk-android-ndk") +cmake_minimum_required(VERSION 3.22.1) +# use the C++ 17 compiler +set(CMAKE_CXX_STANDARD 17) +add_subdirectory(src/main/cpp) + +if(${CMAKE_BUILD_TYPE} STREQUAL Debug) + enable_testing() + add_subdirectory(src/test/cpp) +endif() diff --git a/features/dd-sdk-android-ndk/README.md b/features/dd-sdk-android-ndk/README.md new file mode 100644 index 0000000000..27f8526d45 --- /dev/null +++ b/features/dd-sdk-android-ndk/README.md @@ -0,0 +1,48 @@ +# Datadog Android Native Crash Collection + +Send crash report for issues rising from the C/C++ code in your application. + +**Note**: Native crash reports can be sent to both RUM and Logs, so you need to add these modules as well. + +## Setup + +```groovy +dependencies { + // if you want to send native crash reports to RUM product + implementation "com.datadoghq:dd-sdk-android-rum:x.x.x" + // if you want to send native crash reports to Logs product + implementation "com.datadoghq:dd-sdk-android-logs:x.x.x" + + implementation "com.datadoghq:dd-sdk-android-ndk:x.x.x" +} +``` + +1. If you want to send native crash reports to RUM, [add and initialize RUM product in your application][1]. +2. If you want to sent native crash reports to Logs, [add and initialize Logs product in your application][2]. +3. Initialize NDK Crash Reporting feature in your application: + +```kotlin +class SampleApplication : Application() { + override fun onCreate() { + super.onCreate() + + // RUM and/or Logs were initialized before + NdkCrashReports.enable() + } +} +``` + +```java +public class SampleApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + + // RUM and/or Logs were initialized before + NdkCrashReports.enable(); + } +} +``` + +[1]: https://docs.datadoghq.com/real_user_monitoring/android/?tab=kotlin +[2]: https://docs.datadoghq.com/logs/log_collection/android/?tab=kotlin diff --git a/features/dd-sdk-android-ndk/api/apiSurface b/features/dd-sdk-android-ndk/api/apiSurface new file mode 100644 index 0000000000..3c528c7e0a --- /dev/null +++ b/features/dd-sdk-android-ndk/api/apiSurface @@ -0,0 +1,2 @@ +object com.datadog.android.ndk.NdkCrashReports + fun enable(com.datadog.android.api.SdkCore = Datadog.getInstance()) diff --git a/features/dd-sdk-android-ndk/api/dd-sdk-android-ndk.api b/features/dd-sdk-android-ndk/api/dd-sdk-android-ndk.api new file mode 100644 index 0000000000..2b07a02381 --- /dev/null +++ b/features/dd-sdk-android-ndk/api/dd-sdk-android-ndk.api @@ -0,0 +1,7 @@ +public final class com/datadog/android/ndk/NdkCrashReports { + public static final field INSTANCE Lcom/datadog/android/ndk/NdkCrashReports; + public static final fun enable ()V + public static final fun enable (Lcom/datadog/android/api/SdkCore;)V + public static synthetic fun enable$default (Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V +} + diff --git a/features/dd-sdk-android-ndk/build.gradle.kts b/features/dd-sdk-android-ndk/build.gradle.kts new file mode 100644 index 0000000000..0538cc5019 --- /dev/null +++ b/features/dd-sdk-android-ndk/build.gradle.kts @@ -0,0 +1,114 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import com.datadog.gradle.Dependencies +import com.datadog.gradle.config.AndroidConfig +import com.datadog.gradle.config.androidLibraryConfig +import com.datadog.gradle.config.dependencyUpdateConfig +import com.datadog.gradle.config.detektCustomConfig +import com.datadog.gradle.config.javadocConfig +import com.datadog.gradle.config.junitConfig +import com.datadog.gradle.config.kotlinConfig +import com.datadog.gradle.config.publishingConfig +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + // Build + id("com.android.library") + kotlin("android") + + // Publish + `maven-publish` + signing + id("org.jetbrains.dokka-javadoc") + + // Analysis tools + id("com.github.ben-manes.versions") + + // Tests + id("org.jetbrains.kotlinx.kover") + + // Internal Generation + id("com.datadoghq.dependency-license") + id("apiSurface") + id("transitiveDependencies") + id("verificationXml") + id("binary-compatibility-validator") +} + +android { + + defaultConfig { + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + version = Dependencies.Versions.CMake + } + } + } + + testOptions { + targetSdk = AndroidConfig.TARGET_SDK + } + + namespace = "com.datadog.android.ndk" + + externalNativeBuild { + cmake { + path = File("$projectDir/CMakeLists.txt") + version = Dependencies.Versions.CMake + } + } + + ndkVersion = Dependencies.Versions.Ndk +} + +dependencies { + implementation(project(":dd-sdk-android-internal")) + api(project(":dd-sdk-android-core")) + implementation(libs.kotlin) + implementation(libs.okHttp) + implementation(libs.androidXMultidex) + + testImplementation(project(":tools:unit")) { + attributes { + attribute( + com.android.build.api.attributes.ProductFlavorAttr.of("platform"), + objects.named("jvm") + ) + } + } + testImplementation(libs.bundles.jUnit5) + testImplementation(libs.bundles.testTools) + + androidTestImplementation(project(":tools:unit")) { + attributes { + attribute( + com.android.build.api.attributes.ProductFlavorAttr.of("platform"), + objects.named("art") + ) + } + } + androidTestImplementation(libs.bundles.integrationTests) + androidTestImplementation(libs.gson) + androidTestImplementation(libs.assertJ) + + if (project.hasProperty(com.datadog.gradle.Properties.USE_API21_JAVA_BACKPORT)) { + // this is needed to make AssertJ working on APIs <24 + androidTestImplementation(project(":tools:javabackport")) + } +} + +kotlinConfig(jvmBytecodeTarget = JvmTarget.JVM_11) +androidLibraryConfig() +junitConfig() +javadocConfig() +dependencyUpdateConfig() +publishingConfig( + "An NDK integration to use with the Datadog monitoring library for Android applications." +) +detektCustomConfig() diff --git a/features/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/NdkTests.kt b/features/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/NdkTests.kt new file mode 100644 index 0000000000..825cbd9c81 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/androidTest/kotlin/com/datadog/android/ndk/NdkTests.kt @@ -0,0 +1,238 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.datadog.tools.unit.ConditionWatcher +import com.datadog.tools.unit.assertj.JsonObjectAssert.Companion.assertThat +import com.google.gson.JsonParser +import fr.xgouchet.elmyr.junit4.ForgeRule +import org.assertj.core.api.Assertions +import org.assertj.core.data.Offset +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import java.lang.RuntimeException +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class NdkTests { + + @get:Rule + val forge = ForgeRule() + + @get:Rule + val temporaryFolder = TemporaryFolder() + + companion object { + init { + System.loadLibrary("datadog-native-lib") + System.loadLibrary("datadog-native-lib-test") + } + } + + @Test + fun ndkSuitTests() { + if (runNdkSuitTests() != 0) { + throw RuntimeException("NDK Suit tests failed") + } + } + + @Test + fun ndkStandaloneTests() { + if (runNdkStandaloneTests() != 0) { + throw RuntimeException("NDK Standalone tests failed") + } + } + + @Test + fun mustWriteAnErrorLog_whenHandlingSignal_whenConsentUpdatedToGranted() { + val fakeSignal = forge.aPositiveInt(true) + val fakeSignalName = forge.anAlphabeticalString() + val fakeErrorMessage = forge.anAlphabeticalString() + val fakeErrorStack = forge.anAlphabeticalString() + initNdkErrorHandler(temporaryFolder.root.absolutePath) + updateTrackingConsent(1) + val fakeAppStartTimeMs = forge.aLong(min = 0L, max = System.currentTimeMillis()) + updateAppStartTime(fakeAppStartTimeMs) + + val expectedTimestamp = System.currentTimeMillis() + val expectedTimeSinceAppStartMs = expectedTimestamp - fakeAppStartTimeMs + simulateSignalInterception( + fakeSignal, + fakeSignalName, + fakeErrorMessage, + fakeErrorStack + ) + + // we need to give time to native part to write the file + // otherwise we will get into race condition issues + ConditionWatcher { + // assert the log file + val listFiles = temporaryFolder.root.listFiles() + val inputStream = listFiles?.first()?.inputStream() + inputStream?.use { + val jsonString = String(it.readBytes(), Charset.forName("utf-8")) + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + assertThat(jsonObject).hasField("signal", fakeSignal) + assertThat(jsonObject).hasField("signal_name", fakeSignalName) + assertThat(jsonObject).hasField("message", fakeErrorMessage) + assertThat(jsonObject).hasField("stacktrace", fakeErrorStack) + assertThat(jsonObject).hasField( + "timestamp", + expectedTimestamp, + Offset.offset(TimeUnit.SECONDS.toMillis(2)) + ) + assertThat(jsonObject).hasField( + "time_since_app_start_ms", + expectedTimeSinceAppStartMs, + Offset.offset(TimeUnit.SECONDS.toMillis(2)) + ) + } + true + }.doWait(timeoutMs = 5000) + } + + @Test + fun mustNotWriteAnyLog_whenHandlingSignal_whenConsentUpdatedToPending() { + val fakeSignal = forge.aPositiveInt(true) + val fakeSignalName = forge.anAlphabeticalString() + val fakeErrorMessage = forge.anAlphabeticalString() + val fakeErrorStack = forge.anAlphabeticalString() + initNdkErrorHandler(temporaryFolder.root.absolutePath) + updateTrackingConsent(0) + simulateSignalInterception( + fakeSignal, + fakeSignalName, + fakeErrorMessage, + fakeErrorStack + ) + + // we need to give time to native part to write the file + // otherwise we will get into race condition issues + ConditionWatcher { + // assert the log file is not written + Assertions.assertThat(temporaryFolder.root.listFiles()).isEmpty() + true + }.doWait(timeoutMs = 5000) + } + + @Test + fun mustNotWriteAnyLog_whenHandlingSignal_whenConsentUpdatedToNotGranted() { + val fakeSignal = forge.aPositiveInt(true) + val fakeSignalName = forge.anAlphabeticalString() + val fakeErrorMessage = forge.anAlphabeticalString() + val fakeErrorStack = forge.anAlphabeticalString() + initNdkErrorHandler(temporaryFolder.root.absolutePath) + updateTrackingConsent(2) + simulateSignalInterception( + fakeSignal, + fakeSignalName, + fakeErrorMessage, + fakeErrorStack + ) + + // we need to give time to native part to write the file + // otherwise we will get into race condition issues + ConditionWatcher { + // assert the log file is not written + Assertions.assertThat(temporaryFolder.root.listFiles()).isEmpty() + true + }.doWait(timeoutMs = 5000) + } + + @Test + fun mustNotWriteAnyLog_whenHandlingSignal_mutexInDatadogNativeLibCannotBeAcquired() { + val fakeSignal = forge.aPositiveInt(true) + val fakeSignalName = forge.anAlphabeticalString() + val fakeErrorMessage = forge.anAlphabeticalString() + val fakeErrorStack = forge.anAlphabeticalString() + initNdkErrorHandler(temporaryFolder.root.absolutePath) + updateTrackingConsent(2) + simulateFailedSignalInterception( + fakeSignal, + fakeSignalName, + fakeErrorMessage, + fakeErrorStack + ) + + // we need to give time to native part to write the file + // otherwise we will get into race condition issues + ConditionWatcher { + // assert the log file + Assertions.assertThat(temporaryFolder.root.listFiles()).isEmpty() + true + }.doWait(timeoutMs = 5000) + } + + // region NDK + + /** + * Will run the actual test suites on the NDK side. + * @return 0 if all the tests passed. + */ + private external fun runNdkSuitTests(): Int + + /** + * Will run the singular tests on the NDK side. + * @return 0 if all the tests passed. + */ + private external fun runNdkStandaloneTests(): Int + + /** + * Will initialize the NDK crash reporter. + * @param storageDir the storage directory for the reported crash logs + */ + private external fun initNdkErrorHandler( + storageDir: String + ) + + /** + * Simulate a signal interception into the NDK crash reporter. + * @param signal the signal id (a positive int) + * @param signalName the signal name (e.g. SIGHUP, SIGINT, SIGILL, etc.) + * @param errorMessage the error message + * @param errorStack the error stack + */ + private external fun simulateSignalInterception( + signal: Int, + signalName: String, + errorMessage: String, + errorStack: String + ) + + /** + * Simulate a failed signal interception into the NDK crash reporter because mutex lock + * was already acquired by other thread. + * @param signal the signal id (a positive int) + * @param signalName the signal name (e.g. SIGHUP, SIGINT, SIGILL, etc.) + * @param errorMessage the error message + * @param errorStack the error stack + */ + private external fun simulateFailedSignalInterception( + signal: Int, + signalName: String, + errorMessage: String, + errorStack: String + ) + + /** + * Updates the tracking consent into the NDK crash reporter. + * @param consent as the tracking consent value (0 - PENDING, 1 - GRANTED, 2 - NOT-GRANTED) + */ + private external fun updateTrackingConsent( + consent: Int + ) + + private external fun updateAppStartTime( + appStartTimeMs: Long + ) + + // endregion +} diff --git a/features/dd-sdk-android-ndk/src/main/cpp/CMakeLists.txt b/features/dd-sdk-android-ndk/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..b8a4b9153e --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/CMakeLists.txt @@ -0,0 +1,37 @@ +include_directories( + ./ + utils + model +) +add_library( # Sets the name of the library. + datadog-native-lib + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + datadog-native-lib.cpp + datadog-native-lib.h + utils/signal-monitor.c + utils/signal-monitor.h + utils/file-utils.cpp + utils/file-utils.h + utils/string-utils.cpp + utils/string-utils.h + utils/datetime-utils.cpp + utils/datetime-utils.h + utils/backtrace-handler.cpp + utils/backtrace-handler.h + ) +find_library( # Sets the name of the path variable. + log-lib + # Specifies the name of the NDK library that + # you want CMake to locate. + log) +target_link_libraries( # Specifies the target library. + datadog-native-lib + # Links the target library to the log library + # included in the NDK. + ${log-lib}) +set_target_properties(datadog-native-lib + PROPERTIES + COMPILE_OPTIONS + -Werror -Wall -pedantic) diff --git a/features/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.cpp b/features/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.cpp new file mode 100644 index 0000000000..2a66ed0c9a --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.cpp @@ -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 2016-Present Datadog, Inc. + */ + +#include "datadog-native-lib.h" + +#include +#include +#include +#include +#include + +#include "android/log.h" +#include "backtrace-handler.h" +#include "datetime-utils.h" +#include "file-utils.h" +#include "signal-monitor.h" +#include "string-utils.h" + +static struct Context { + std::string storage_dir; + + Context() : storage_dir() {} +} main_context; + + +static const char *LOG_TAG = "DatadogNdkCrashReporter"; + +static pthread_mutex_t handler_mutex = PTHREAD_MUTEX_INITIALIZER; +static const uint8_t tracking_consent_pending = 0; +static const uint8_t tracking_consent_granted = 1; +static uint8_t tracking_consent = tracking_consent_pending; // 0 - PENDING, 1 - GRANTED, 2 - NOT-GRANTED +static uint64_t global_app_start_time_millis = 0; + +#ifndef NDEBUG + +void lockMutex() { + pthread_mutex_lock(&handler_mutex); +} + +void unlockMutex() { + pthread_mutex_unlock(&handler_mutex); +} + +#endif + +std::string serialize_crash_report(int signum, uint64_t timestamp, const char* signal_name, const char* error_message, const char* error_stacktrace) { + static const char* json_formatter = R"({"signal":%s,"timestamp":%s,"time_since_app_start_ms":%s,"signal_name":"%s","message":"%s","stacktrace":"%s"})"; + const uint64_t time_since_app_start = timestamp - global_app_start_time_millis; + std::string serialized_log = stringutils::format(json_formatter, + std::to_string(signum).c_str(), + std::to_string(timestamp).c_str(), + std::to_string(time_since_app_start).c_str(), + signal_name, + error_message, + error_stacktrace); + return serialized_log; +} + +void write_crash_report(int signum, + const char *signal_name, + const char *error_message, + const char *error_stacktrace) { + using namespace std; + static const std::string crash_log_filename = "crash_log"; + + // sync everything + if (tracking_consent != tracking_consent_granted) { + return; + } + + if(pthread_mutex_trylock(&handler_mutex) != 0){ + // There is no action to take if the mutex cannot be acquired. + // In this case will fail to write the crash log and return here in order not + // to block the process and create possible ANRs. + return; + } + + if (main_context.storage_dir.empty()) { + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, + "The crash reports storage directory file path was null"); + pthread_mutex_unlock(&handler_mutex); + return; + } + + // create crash reporting directory if it does not exist + if (!fileutils::create_dir_if_not_exists(main_context.storage_dir.c_str())) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, + "Was unable to create the NDK reports storage directory: %s", + main_context.storage_dir.c_str()); + pthread_mutex_unlock(&handler_mutex); + return; + } + + // serialize the log + const string file_path = main_context.storage_dir.append("/").append(crash_log_filename); + const uint64_t timestamp = time_since_epoch(); + const std::string serialized_log = serialize_crash_report(signum, timestamp, signal_name, error_message, error_stacktrace); + + // write the log in the crash log file + ofstream logs_file_output_stream(file_path.c_str(), + ofstream::out | ofstream::trunc); + if (logs_file_output_stream.is_open()) { + logs_file_output_stream << serialized_log.c_str(); + } + logs_file_output_stream.close(); + pthread_mutex_unlock(&handler_mutex); +} + +void update_main_context(JNIEnv *env, + jstring storage_path) { + using namespace stringutils; + if(pthread_mutex_trylock(&handler_mutex) != 0){ + // There is no action to take if the mutex cannot be acquired. Probably int this case + // there is already a log writing due to a crash in progress. + // In this case updating the context will not make sense anymore and we do not want to + // stale the process here. + return; + } + main_context.storage_dir = copy_to_string(env, storage_path); + pthread_mutex_unlock(&handler_mutex); +} + +void update_tracking_consent(jint consent) { + tracking_consent = (uint8_t) consent; +} + +void update_app_start_time_millis(jlong time_ms) { + global_app_start_time_millis = time_ms; +} + +/// Jni bindings +extern "C" JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_internal_NdkCrashReportsFeature_registerSignalHandler( + JNIEnv *env, + jobject /* this */, + jstring storage_path, + jint consent, + jlong app_start_time_millis) { + + update_main_context(env, storage_path); + update_tracking_consent(consent); + update_app_start_time_millis(app_start_time_millis); + start_monitoring(); +} + + +extern "C" JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_internal_NdkCrashReportsFeature_unregisterSignalHandler( + JNIEnv *env, + jobject /* this */) { + stop_monitoring(); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_internal_NdkCrashReportsFeature_updateTrackingConsent( + JNIEnv *env, + jobject /* this */, + jint consent) { + update_tracking_consent(consent); +} \ No newline at end of file diff --git a/features/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.h b/features/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.h new file mode 100644 index 0000000000..a0f6b3ca58 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/datadog-native-lib.h @@ -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 2016-Present Datadog, Inc. + */ + +#include + +#ifndef DATADOG_NATIVE_LIB_H +#define DATADOG_NATIVE_LIB_H + +#ifdef __cplusplus +extern "C" { +#endif + +void update_main_context(JNIEnv *env, + jstring storage_path); + +void update_tracking_consent(jint consent); + +void update_app_start_time_millis(jlong time_ms); + +void write_crash_report(int signum, + const char *signal_name, + const char *error_message, + const char *error_stacktrace); + +#ifndef NDEBUG +void lockMutex(); +void unlockMutex(); +#endif + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/features/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.cpp b/features/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.cpp new file mode 100644 index 0000000000..8dc06bdf19 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.cpp @@ -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 2016-Present Datadog, Inc. + */ + +#include "backtrace-handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct BacktraceState { + uintptr_t *current; + uintptr_t *end; +}; + +namespace { + _Unwind_Reason_Code unwind_callback(struct _Unwind_Context *context, void *arg) { + auto *state = static_cast(arg); + uintptr_t pointer_to_stack_line = _Unwind_GetIP(context); + if (pointer_to_stack_line) { + // we have reached the end of the given buffer size so we stop the unwind loop + if (state->current == state->end) { + return _URC_END_OF_STACK; + } else { + // we set the state->current to current+1 and we set + // its value as the pointer of the current stack line + *state->current++ = pointer_to_stack_line; + } + } + return _URC_NO_REASON; + } + + size_t capture_backtrace(uintptr_t *buffer, size_t max) { + BacktraceState state = {buffer, buffer + max}; + // unwinds the backtrace and fills the buffer with stack lines addresses + _Unwind_Backtrace(unwind_callback, &state); + return state.current - buffer; + } + + void append_hex_address_no_prefix(uintptr_t address, std::string& string) { + char address_as_hexa[20]; + // The ARM_32 processors will use an unsigned long long to represent a pointer so we will choose the + // String format that fits both ARM_32 and ARM_64 (lx). +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat" + std::snprintf(address_as_hexa, sizeof(address_as_hexa), "%08lx", address); +#pragma clang diagnostic pop + string.append(address_as_hexa); + } + + void get_info_from_address(size_t index, + const uintptr_t address, + std::string& backtrace) { + Dl_info info; + int fetch_info_success = dladdr(reinterpret_cast(address), &info); + backtrace.append(std::to_string(index)); + backtrace.append(" pc "); + // No reason to output relative pointers if we don't have both the name + // and base address of the shared object + if (fetch_info_success && info.dli_fbase && info.dli_fname) { + uintptr_t offset = address - (uintptr_t)info.dli_fbase; + append_hex_address_no_prefix(offset, backtrace); + + backtrace.append(" "); + backtrace.append(info.dli_fname); + + if (info.dli_sname) { + backtrace.append(" ("); + backtrace.append(info.dli_sname); + + if (info.dli_saddr) { + uintptr_t symbol_offset = address - (uintptr_t)info.dli_saddr; + backtrace.append("+"); + backtrace.append(std::to_string(symbol_offset)); + } + backtrace.append(")"); + } + } else { + append_hex_address_no_prefix(address, backtrace); + } + + backtrace.append("\\n"); + } +} + +bool copyString(const std::string &str, char *ptr, size_t max_size) { + size_t str_size = str.size(); + size_t copy_size = std::min(str_size, max_size - 1); + memcpy(ptr, str.data(), copy_size); + ptr[str.size()] = '\0'; + return copy_size == str_size; +} + +bool generate_backtrace(char *backtrace_ptr, size_t start_index, size_t max_size) { + // define the buffer which will hold pointers to stack memory addresses + uintptr_t buffer[max_stack_frames]; + // we will now unwind the stack and capture all the memory addresses up to max_stack_frames in + // the buffer + const size_t number_of_captured_frames = capture_backtrace(buffer, max_stack_frames); + std::string backtrace; + backtrace.reserve(max_size); + for (size_t idx = start_index; idx < number_of_captured_frames; ++idx) { + // we will iterate through all the stack addresses and translate each address in + // readable information + get_info_from_address(idx - start_index, buffer[idx], backtrace); + if (backtrace.length() > max_size) { + // No reason to continue, we're at the max size we're willing to dedicate to + // this string + break; + } + } + return copyString(backtrace, backtrace_ptr, max_size); +} + + + + diff --git a/features/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.h b/features/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.h new file mode 100644 index 0000000000..fbcc307525 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/backtrace-handler.h @@ -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 2016-Present Datadog, Inc. + */ + + +#ifndef BACKTRACE_HANDLER_H +#define BACKTRACE_HANDLER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +const size_t stack_frames_start_index = 3; +const size_t max_stack_frames = 70 + stack_frames_start_index; +const size_t max_characters_per_stack_frame = 500; +const size_t max_stack_size = max_stack_frames * max_characters_per_stack_frame; + +// We cannot use a namespace here as this function will be called from C file (signal_monitor.c) +bool generate_backtrace(char *backtrace_ptr, size_t start_index, size_t max_size); + +#ifdef __cplusplus +} +#endif +#endif \ No newline at end of file diff --git a/features/dd-sdk-android-ndk/src/main/cpp/utils/datetime-utils.cpp b/features/dd-sdk-android-ndk/src/main/cpp/utils/datetime-utils.cpp new file mode 100644 index 0000000000..38420e196d --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/datetime-utils.cpp @@ -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 2016-Present Datadog, Inc. + */ + +#include "datetime-utils.h" + +#include +#include +#include +#include +#include + +uint64_t time_since_epoch() { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); +} + +void format_date(char *buffer, size_t buffer_size, const char *format) { + using namespace std::chrono; + const std::time_t t = time(nullptr); + const struct tm *timeinfo = gmtime(&t); + strftime(buffer, buffer_size, format, timeinfo); +} \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/datetime.h b/features/dd-sdk-android-ndk/src/main/cpp/utils/datetime-utils.h similarity index 100% rename from dd-sdk-android-ndk/src/main/cpp/utils/datetime.h rename to features/dd-sdk-android-ndk/src/main/cpp/utils/datetime-utils.h diff --git a/features/dd-sdk-android-ndk/src/main/cpp/utils/file-utils.cpp b/features/dd-sdk-android-ndk/src/main/cpp/utils/file-utils.cpp new file mode 100644 index 0000000000..610c291d6e --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/file-utils.cpp @@ -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 2016-Present Datadog, Inc. + */ + +#include "file-utils.h" + +#include +#include +#include + +namespace fileutils { + + bool create_dir_if_not_exists(const char *dirPath) { + if (opendir(dirPath) == nullptr && ENOENT == errno) { + // directory does not exist. We will create it. + return mkdir(dirPath, S_IRWXU) == 0; + } + + return true; // the directory was already there + } +} + diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/fileutils.h b/features/dd-sdk-android-ndk/src/main/cpp/utils/file-utils.h similarity index 100% rename from dd-sdk-android-ndk/src/main/cpp/utils/fileutils.h rename to features/dd-sdk-android-ndk/src/main/cpp/utils/file-utils.h diff --git a/features/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.c b/features/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.c new file mode 100644 index 0000000000..342d3d6795 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.c @@ -0,0 +1,273 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +#include "signal-monitor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "backtrace-handler.h" +#include "datadog-native-lib.h" + +static const char *LOG_TAG = "DatadogNdkCrashReporter"; +static stack_t signal_stack; + +/* the original sigaction array which will hold the original handlers for each overridden signal */ +volatile struct sigaction *volatile original_sigactions = NULL; + +/* pre-allocated memory for working with the backtrace */ +char* backtrace_scratch = NULL; + +volatile bool handlers_installed = false; + +// for testing purposes +#ifndef NDEBUG +int performed_install_ops = 0; +int performed_uninstall_ops = 0; +#endif + +void recordInstallOp() { +#ifndef NDEBUG + performed_install_ops++; +#endif +} + +void recordUninstallOp() { +#ifndef NDEBUG + performed_uninstall_ops++; +#endif +} + +/** + * Native signals which will be captured by the signal handler™ + */ + +struct signal { + int signal_value; + char *signal_name; + char *signal_error_message; +}; + +static const struct signal handled_signals[] = { + {SIGILL, "SIGILL", "Illegal instruction"}, + {SIGBUS, "SIGBUS", "Bus error (bad memory access)"}, + {SIGFPE, "SIGFPE", "Floating-point exception"}, + {SIGABRT, "SIGABRT", "The process was terminated"}, + {SIGSEGV, "SIGSEGV", "Segmentation violation (invalid memory reference)"}, + {SIGTRAP, "SIGTRAP", "Signal trap"} +}; + +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + +size_t handled_signals_size() { return sizeof(handled_signals) / sizeof(handled_signals[0]); } + + +void free_up_memory() { + if (backtrace_scratch != NULL) { + free((void*) backtrace_scratch); + backtrace_scratch = NULL; + } + if (original_sigactions != NULL) { + free((void *) original_sigactions); + original_sigactions = NULL; + } + if (signal_stack.ss_sp != NULL) { + free(signal_stack.ss_sp); + signal_stack.ss_sp = NULL; + } +} + + +void invoke_previous_handler(int signum, siginfo_t *info, void *user_context) { + // It may happen that the process is killed during this function execution. + // Therefore this function may never return. + + if(pthread_mutex_trylock(&mutex) != 0){ + // There is no action to take if the mutex cannot be acquired. + // In this case will just return here in order not to block the process. + return; + } + + const size_t signals_array_size = handled_signals_size(); + for (int i = 0; i < signals_array_size; ++i) { + const int signal = handled_signals[i].signal_value; + if (signal == signum) { + struct sigaction previous = original_sigactions[i]; + // From sigaction(2): + // > If act is non-zero, it specifies an action (SIG_DFL, SIG_IGN, or a + // handler routine) + if (previous.sa_flags & SA_SIGINFO) { + // This handler can handle signal number, info, and user context + // (POSIX). From sigaction(2): > If this bit is set, the handler + // function is assumed to be pointed to by the sa_sigaction member of + // struct sigaction and should match the proto- type shown above or as + // below in EXAMPLES. This bit should not be set when assigning SIG_DFL + // or SIG_IGN. + previous.sa_sigaction(signum, info, user_context); + } else if (previous.sa_handler == SIG_DFL) { + // raise to trigger the default handler. It cannot be called directly. + raise(signum); + + } else if (previous.sa_handler != SIG_IGN) { + // This handler can only handle to signal number (ANSI C) + void (*previous_handler)(int) = previous.sa_handler; + previous_handler(signum); + } + } + } + + pthread_mutex_unlock(&mutex); +} + +void uninstall_handlers() { + if (!handlers_installed) { + return; + } + const size_t signals_array_size = handled_signals_size(); + for (int i = 0; i < signals_array_size; i++) { + volatile struct sigaction *volatile original_action = &original_sigactions[i]; + if (original_action) { + const int signal = handled_signals[i].signal_value; + sigaction(signal, (struct sigaction *) original_action, 0); + } + } + recordUninstallOp(); + handlers_installed = false; +} + +void handle_signal(int signum, siginfo_t *info, void *user_context) { + const size_t signals_array_size = handled_signals_size(); + for (int i = 0; i < signals_array_size; i++) { + const int signal = handled_signals[i].signal_value; + if (signal == signum) { + + // in case the stacktrace is bigger than the required size it will be truncated + // because we are unwinding the stack strace at this level we are always going to + // to have for the top 3 levels at the top of the trace the executed lines from our + // library: handle_signal, generate_backtrace and capture_backtrace. + // If you are changing this code make sure to start from the new frame + // index when generating the backtrace. + generate_backtrace(backtrace_scratch, stack_frames_start_index, max_stack_size); + + + write_crash_report(signal, + handled_signals[i].signal_name, + handled_signals[i].signal_error_message, + backtrace_scratch); + + break; + } + } + + // We need to uninstall our custom handlers otherwise we will go in a continuous loop when + // calling the prev handler (sigaction) with this signum. + uninstall_handlers(); + invoke_previous_handler(signum, info, user_context); +} + +bool configure_signal_stack() { + + // we increase the intercepted signal stack size to be able to handle the memory allocation + // to unwind the backstack. By using the SA_ONSTACK flag in the sigaction below, + // we are specifically requesting that the signal handler should be executed in a freshly new + // signal stack for which we allocate extra memory here. + // Art is already allocating memory for the signal stack here: + // https://android.googlesource.com/platform/art/+/master/runtime/thread_linux.cc#35) but + // because we need a bit more than that we will re - allocate it on our end. + size_t expected_stack_size = 32 * 1024; // 32K -- same as the ART but on our own dedicated stack + size_t stack_size = expected_stack_size < MINSIGSTKSZ ? MINSIGSTKSZ : expected_stack_size; + if ((signal_stack.ss_sp = calloc(1, stack_size)) == NULL) { + return false; + } + signal_stack.ss_size = stack_size; + signal_stack.ss_flags = 0; + if (sigaltstack(&signal_stack, 0) < 0) { + free(signal_stack.ss_sp); + signal_stack.ss_sp=NULL; + return false; + } + + // Create a scratch area for grabbing the backtrace + backtrace_scratch = malloc(max_stack_size * sizeof(char)); + if (backtrace_scratch == NULL) { + return false; + } + + return true; +} + +bool override_native_signal_handlers() { + struct sigaction datadog_sigaction = {}; + + if (sigemptyset((sigset_t *) &datadog_sigaction.sa_mask) != 0) { + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, + "Not able to initialize the Datadog signal handler"); + return false; + } + datadog_sigaction.sa_sigaction = handle_signal; + // we will use the SA_ONSTACK mask here to handle the signal in a freshly new signal stack. + datadog_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK; + + const size_t signals_array_size = handled_signals_size(); + original_sigactions = calloc(signals_array_size, sizeof(struct sigaction)); + if (original_sigactions == NULL) { + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, + "Not able to allocate the memory to persist the original handlers"); + return false; + } + for (int i = 0; i < signals_array_size; i++) { + const int signal = handled_signals[i].signal_value; + int success = sigaction(signal, &datadog_sigaction, + (struct sigaction *) &original_sigactions[i]); + if (success != 0) { + __android_log_print(ANDROID_LOG_ERROR, + LOG_TAG, + "Not able to catch the signal: %d", + signal); + } + } + recordInstallOp(); + return true; +} + +bool try_to_install_handlers() { + if (handlers_installed) { + return true; + } + handlers_installed = configure_signal_stack() && override_native_signal_handlers(); + return handlers_installed; +} + +bool start_monitoring() { + pthread_mutex_lock(&mutex); + bool installed = try_to_install_handlers(); + if (installed) { + __android_log_write(ANDROID_LOG_INFO, LOG_TAG, + "Successfully installed Datadog NDK signal handlers"); + } else { + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, + "Unable to install Datadog NDK signal handlers"); + } + pthread_mutex_unlock(&mutex); + return installed; +} + +void stop_monitoring() { + pthread_mutex_lock(&mutex); + uninstall_handlers(); + free_up_memory(); + pthread_mutex_unlock(&mutex); +} + diff --git a/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.h b/features/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.h similarity index 90% rename from dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.h rename to features/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.h index e7557fde9e..de61ea316e 100644 --- a/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.h +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/signal-monitor.h @@ -4,7 +4,6 @@ * Copyright 2016-Present Datadog, Inc. */ - #ifndef SIGNAL_MONITOR_H #define SIGNAL_MONITOR_H @@ -20,13 +19,13 @@ extern "C" { * serialize to disk and invoke the previously-installed handler * @return true if monitoring started successfully */ -bool install_signal_handlers(); +bool start_monitoring(); /** * Stop monitoring for fatal exceptions and reinstall previously-installed * handlers */ -void uninstall_signal_handlers(void); +void stop_monitoring(void); #ifdef __cplusplus } diff --git a/features/dd-sdk-android-ndk/src/main/cpp/utils/string-utils.cpp b/features/dd-sdk-android-ndk/src/main/cpp/utils/string-utils.cpp new file mode 100644 index 0000000000..d6a3d733b2 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/string-utils.cpp @@ -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 2016-Present Datadog, Inc. + */ + +#include "string-utils.h" + +#include +#include +#include + +namespace stringutils { + + std::string copy_to_string(JNIEnv *env, jstring from) { + if (from == nullptr) { + return std::string(); + } + + const char *raw_str = env->GetStringUTFChars(from, 0); + + std::string result(raw_str); + + env->ReleaseStringUTFChars(from, raw_str); + + return result; + } + +} diff --git a/features/dd-sdk-android-ndk/src/main/cpp/utils/string-utils.h b/features/dd-sdk-android-ndk/src/main/cpp/utils/string-utils.h new file mode 100644 index 0000000000..cc1aa9eaf7 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/cpp/utils/string-utils.h @@ -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 2016-Present Datadog, Inc. + */ + + +#include +#include + + +#ifndef STRINGUTILS_H +#define STRINGUTILS_H + + +namespace stringutils { + std::string copy_to_string(JNIEnv *env, jstring from); + + template + std::string format(const char* formatter, Args... args) { + // first we compute the size of the buffer required to format this string + // we will add 1 at the end for the end of string /0 character + const size_t size = snprintf(nullptr, 0, formatter, args...); + char buffer[size + 1]; + snprintf(buffer, size + 1, formatter, args...); + return std::string(buffer, buffer + size); + } +} + +#endif + diff --git a/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/NdkCrashReports.kt b/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/NdkCrashReports.kt new file mode 100644 index 0000000000..78cbb86d85 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/NdkCrashReports.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk + +import com.datadog.android.Datadog +import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.ndk.internal.NdkCrashReportsFeature + +/** + * An entry point to Datadog NDK Crash Reports feature. + */ +object NdkCrashReports { + + /** + * Enables a NDK Crash Reports feature. + * + * @param sdkCore SDK instance to register feature in. If not provided, default SDK instance + * will be used. + */ + @JvmOverloads + @JvmStatic + fun enable(sdkCore: SdkCore = Datadog.getInstance()) { + val ndkCrashReportsFeature = NdkCrashReportsFeature(sdkCore as FeatureSdkCore) + + sdkCore.registerFeature(ndkCrashReportsFeature) + } +} diff --git a/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeature.kt b/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeature.kt new file mode 100644 index 0000000000..7862619c7c --- /dev/null +++ b/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeature.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import android.content.Context +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.internal.utils.allowThreadDiskReads +import com.datadog.android.internal.utils.allowThreadDiskWrites +import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.privacy.TrackingConsentProviderCallback +import java.io.File +import java.lang.NullPointerException +import java.util.concurrent.TimeUnit + +/** + * An implementation of the [Feature] which will allow to intercept and report the + * NDK crashes to our logs dashboard. + */ +internal class NdkCrashReportsFeature( + private val sdkCore: FeatureSdkCore +) : Feature, TrackingConsentProviderCallback { + private var nativeLibraryLoaded = false + + override val name: String = Feature.NDK_CRASH_REPORTS_FEATURE_NAME + + // region Feature + @Suppress("ReturnCount") + override fun onInitialize(appContext: Context) { + loadNativeLibrary(sdkCore.internalLogger) + if (!nativeLibraryLoaded) { + return + } + val internalSdkCore = sdkCore as InternalSdkCore + val rootStorageDir = internalSdkCore.rootStorageDir + if (rootStorageDir == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { NO_SDK_ROOT_DIR_MESSAGE } + ) + return + } + val ndkCrashesDirs = File( + rootStorageDir, + NDK_CRASH_REPORTS_FOLDER + ) + try { + allowThreadDiskWrites { + ndkCrashesDirs.mkdirs() + } + } catch (e: SecurityException) { + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Unable to create NDK Crash Report folder $ndkCrashesDirs" }, + e + ) + return + } + val appStartTimestamp = + TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) - System.nanoTime() + sdkCore.appStartTimeNs + registerSignalHandler( + ndkCrashesDirs.absolutePath, + consentToInt(internalSdkCore.trackingConsent), + TimeUnit.NANOSECONDS.toMillis(appStartTimestamp) + ) + } + + override fun onStop() { + if (!nativeLibraryLoaded) { + return + } + unregisterSignalHandler() + } + + // endregion + + // region TrackingConsentProviderCallback + + override fun onConsentUpdated(previousConsent: TrackingConsent, newConsent: TrackingConsent) { + if (!nativeLibraryLoaded) { + return + } + updateTrackingConsent(consentToInt(newConsent)) + } + + internal fun consentToInt(newConsent: TrackingConsent): Int { + return when (newConsent) { + TrackingConsent.PENDING -> TRACKING_CONSENT_PENDING + TrackingConsent.GRANTED -> TRACKING_CONSENT_GRANTED + else -> TRACKING_CONSENT_NOT_GRANTED + } + } + + // endregion + + // region NDK + + private fun loadNativeLibrary(internalLogger: InternalLogger) { + var exception: Throwable? = null + try { + allowThreadDiskReads { + System.loadLibrary("datadog-native-lib") + } + nativeLibraryLoaded = true + } catch (e: SecurityException) { + exception = e + } catch (@SuppressWarnings("TooGenericExceptionCaught") e: NullPointerException) { + exception = e + } catch (e: UnsatisfiedLinkError) { + exception = e + } + exception?.let { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { ERROR_LOADING_NATIVE_MESSAGE }, + exception + ) + } + } + + private external fun registerSignalHandler( + storagePath: String, + consent: Int, + appStartTimeMs: Long + ) + + private external fun unregisterSignalHandler() + + private external fun updateTrackingConsent(consent: Int) + + // endregion + + internal companion object { + internal const val NDK_CRASH_REPORTS_FOLDER = "ndk_crash_reports_v2" + private const val ERROR_LOADING_NATIVE_MESSAGE = + "We could not load the native library for NDK crash reporting." + internal const val NO_SDK_ROOT_DIR_MESSAGE = + "Cannot get a directory for SDK data storage. Please make sure that SDK is initialized." + internal const val TRACKING_CONSENT_PENDING = 0 + internal const val TRACKING_CONSENT_GRANTED = 1 + internal const val TRACKING_CONSENT_NOT_GRANTED = 2 + } +} diff --git a/features/dd-sdk-android-ndk/src/test/cpp/CMakeLists.txt b/features/dd-sdk-android-ndk/src/test/cpp/CMakeLists.txt new file mode 100644 index 0000000000..d6788becfd --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/CMakeLists.txt @@ -0,0 +1,23 @@ +include_directories( + ../../main/cpp + ../../main/utils + ../cpp/utils +) +add_library( + datadog-native-lib-test + SHARED + integration-tests.cpp + test-crash-log.cpp + test-datetime-utils.cpp + test-format-utils.cpp + test-generate-backtrace.cpp + test-signal-monitor.cpp + test-utils.cpp + test-utils.h +) +find_library( # Sets the name of the path variable. + log-lib + # Specifies the name of the NDK library that + # you want CMake to locate. + log) +target_link_libraries(datadog-native-lib-test datadog-native-lib ${log-lib}) \ No newline at end of file diff --git a/features/dd-sdk-android-ndk/src/test/cpp/integration-tests.cpp b/features/dd-sdk-android-ndk/src/test/cpp/integration-tests.cpp new file mode 100644 index 0000000000..a3b05aa75d --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/integration-tests.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include + + +#include +#include +#include "greatest/greatest.h" +#include "utils/string-utils.h" + + +SUITE (datetime_utils); + +SUITE (backtrace_generation); + +SUITE (signal_monitor); + +SUITE (string_utils); + +SUITE (crash_log); + + +GREATEST_MAIN_DEFS(); + +TEST copy_jni_env_to_string(JNIEnv *jniEnv) { + jstring s = jniEnv->NewStringUTF("test string"); + std::string copied_to = stringutils::copy_to_string(jniEnv, s); + ASSERT_STR_EQ("test string", copied_to.c_str()); + PASS(); +} + +TEST copy_jni_env_to_string_when_source_is_null(JNIEnv *jniEnv) { + std::string copied_to = stringutils::copy_to_string(jniEnv, nullptr); + ASSERT_STR_EQ("", copied_to.c_str()); + PASS(); +} + +int run_jni_env_dependent_tests(JNIEnv *env) { + int argc = 0; + char *argv[] = {}; + GREATEST_MAIN_BEGIN(); + RUN_TEST1(copy_jni_env_to_string, env); + RUN_TEST1(copy_jni_env_to_string_when_source_is_null, env); + GREATEST_MAIN_END(); +} + +int run_test_suites() { + int argc = 0; + char *argv[] = {}; + GREATEST_MAIN_BEGIN(); + RUN_SUITE(datetime_utils); + RUN_SUITE(signal_monitor); + RUN_SUITE(string_utils); + RUN_SUITE(crash_log); + // This test fails on Bitrise on the first backtrace line assertion even and was not able to + // detect why so far. My guess is related with Linux environment, I actually logged the line + // and checked the regEx on top and was passing locally. We will disable this test for now as + // the end to end integration test is passing successfully. + //RUN_SUITE(backtrace_generation); + GREATEST_MAIN_END(); +} + +void test_generate_log( + const int signal, + const char *signal_name, + const char *signal_error_message, + const char *error_stack) { + write_crash_report(signal, signal_name, signal_error_message, error_stack); +} + + +extern "C" JNIEXPORT int JNICALL +Java_com_datadog_android_ndk_NdkTests_runNdkSuitTests(JNIEnv *env, jobject) { + return run_test_suites(); +} + +extern "C" JNIEXPORT int JNICALL +Java_com_datadog_android_ndk_NdkTests_runNdkStandaloneTests(JNIEnv *env, jobject) { + return run_jni_env_dependent_tests(env); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_NdkTests_initNdkErrorHandler( + JNIEnv *env, + jobject thiz, + jstring storage_dir) { + + update_main_context(env, storage_dir); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_NdkTests_simulateSignalInterception( + JNIEnv *env, + jobject thiz, + jint signal, + jstring signal_name, + jstring error_message, + jstring error_stack) { + + const int c_signal = (int) signal; + const char *name = env->GetStringUTFChars(signal_name, 0); + const char *message = env->GetStringUTFChars(error_message, 0); + const char *stack = env->GetStringUTFChars(error_stack, 0); + test_generate_log(c_signal, name, message, stack); + env->ReleaseStringUTFChars(signal_name, name); + env->ReleaseStringUTFChars(error_message, message); + env->ReleaseStringUTFChars(error_stack, stack); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_NdkTests_simulateFailedSignalInterception( + JNIEnv *env, + jobject thiz, + jint signal, + jstring signal_name, + jstring error_message, + jstring error_stack) { + + const int c_signal = (int) signal; + const char *name = env->GetStringUTFChars(signal_name, 0); + const char *message = env->GetStringUTFChars(error_message, 0); + const char *stack = env->GetStringUTFChars(error_stack, 0); + // acquire the mutex to simulate a concurrent signal interception + auto acquire_mutex_lambda = [] { + lockMutex(); + }; + std::thread t1 = std::thread(acquire_mutex_lambda); + t1.join(); + test_generate_log(c_signal, name, message, stack); + env->ReleaseStringUTFChars(signal_name, name); + env->ReleaseStringUTFChars(error_message, message); + env->ReleaseStringUTFChars(error_stack, stack); + // release the mutex to simulate a concurrent signal interception + auto release_mutex_lambda = [] { + unlockMutex(); + }; + std::thread t2 = std::thread(release_mutex_lambda); + t2.join(); +} + + + +extern "C" +JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_NdkTests_updateTrackingConsent( + JNIEnv *env, + jobject /* this */, + jint consent) { + update_tracking_consent(consent); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_datadog_android_ndk_NdkTests_updateAppStartTime( + JNIEnv *env, + jobject /* this */, + jlong app_start_time_ms) { + update_app_start_time_millis(app_start_time_ms); +} + + + diff --git a/features/dd-sdk-android-ndk/src/test/cpp/test-crash-log.cpp b/features/dd-sdk-android-ndk/src/test/cpp/test-crash-log.cpp new file mode 100644 index 0000000000..bde3867c9e --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/test-crash-log.cpp @@ -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 2016-Present Datadog, Inc. + */ + +#include +#include "greatest/greatest.h" +#include "datadog-native-lib.h" + +using namespace std; + +extern std::string serialize_crash_report(int signum, + uint64_t timestamp, + const char* signal_name, + const char* error_message, + const char* error_stacktrace); + +TEST test_will_serialise_the_crash_log(void) { + const char* fake_error_message = "an error message"; + const char* fake_error_stacktrace = "an error stacktrace"; + const int fake_error_signal = 2; + const uint64_t fake_timestamp = 100; + const uint64_t fake_app_start_timestamp = 10; + update_app_start_time_millis(fake_app_start_timestamp); + const char* fake_signal_name = "a signal name"; + const string serialized_log = serialize_crash_report(fake_error_signal, + fake_timestamp, + fake_signal_name, + fake_error_message, + fake_error_stacktrace); + const string expected_serialised_log = string("{\"signal\":") + .append(to_string(fake_error_signal)) + .append(",\"timestamp\":") + .append(to_string(fake_timestamp)) + .append(",\"time_since_app_start_ms\":") + .append(to_string(fake_timestamp - fake_app_start_timestamp)) + .append(",\"signal_name\":") + .append("\"") + .append(fake_signal_name) + .append("\"") + .append(",\"message\":") + .append("\"") + .append(fake_error_message) + .append("\"") + .append(",\"stacktrace\":") + .append("\"") + .append(fake_error_stacktrace) + .append("\"") + .append("}"); + + ASSERT_STR_EQ(serialized_log.c_str(), expected_serialised_log.c_str()); + PASS(); +} + +SUITE (crash_log) { + RUN_TEST(test_will_serialise_the_crash_log); +} + diff --git a/features/dd-sdk-android-ndk/src/test/cpp/test-datetime-utils.cpp b/features/dd-sdk-android-ndk/src/test/cpp/test-datetime-utils.cpp new file mode 100644 index 0000000000..bd5f8345ba --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/test-datetime-utils.cpp @@ -0,0 +1,16 @@ +#include +#include "greatest/greatest.h" +#include "utils/datetime-utils.h" + +TEST test_generate_event_date_format(void) { + char buffer[100]; + format_date(buffer, sizeof(buffer) / sizeof(buffer[0]), "%Y-%m-%d'T'%H:%M:%S.000Z"); + ASSERT(std::regex_match(buffer, std::regex( + "[0-9]{4}-[0-9]{2}-[0-9]{2}'T'[0-9]{2}:[0-9]{2}:[0-9]{2}.000Z"))); + PASS(); +} + + +SUITE (datetime_utils) { + RUN_TEST(test_generate_event_date_format); +} diff --git a/features/dd-sdk-android-ndk/src/test/cpp/test-format-utils.cpp b/features/dd-sdk-android-ndk/src/test/cpp/test-format-utils.cpp new file mode 100644 index 0000000000..cc1cf5f636 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/test-format-utils.cpp @@ -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 2016-Present Datadog, Inc. + */ + +#include +#include +#include "greatest/greatest.h" +#include "utils/string-utils.h" + +TEST test_generates_formatted_string(void) { + const char* formatter = "Given %s will return %s from %s"; + const std::string formatted_string = stringutils::format(formatter, "A", "B", "C"); + const std::string expected_formatted_string = "Given A will return B from C"; + ASSERT_STR_EQ(formatted_string.c_str(), expected_formatted_string.c_str()); + PASS(); +} + +TEST test_does_not_throw_exception_if_not_enough_arguments(void) { + std::runtime_error *expected_error = nullptr; + try { + const std::string formatted_string = stringutils::format("%s %s %s %s", "A", "B", "C"); + } + catch (std::runtime_error &error) { + expected_error = &error; + } + ASSERT(expected_error == nullptr); + PASS(); +} + +TEST test_does_not_throw_exception_if_not_enough_arguments_placeholders(void) { + std::runtime_error *expected_error = nullptr; + try { + const std::string formatted_string = stringutils::format("%s", "A", "B", "C"); + } + catch (std::runtime_error &error) { + expected_error = &error; + } + ASSERT(expected_error == nullptr); + PASS(); +} + +SUITE (string_utils) { + RUN_TEST(test_generates_formatted_string); + // TODO RUMM-0000 disable this one, because snprintf gives SIGSEGV on ARM64 (Apple Silicon) + // strange enough the test just below is ok + // RUN_TEST(test_does_not_throw_exception_if_not_enough_arguments); + RUN_TEST(test_does_not_throw_exception_if_not_enough_arguments_placeholders); +} + diff --git a/features/dd-sdk-android-ndk/src/test/cpp/test-generate-backtrace.cpp b/features/dd-sdk-android-ndk/src/test/cpp/test-generate-backtrace.cpp new file mode 100644 index 0000000000..af1de3ee7b --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/test-generate-backtrace.cpp @@ -0,0 +1,64 @@ +#include "test-utils.h" + +#include +#include +#include + +#include "greatest/greatest.h" +#include "utils/backtrace-handler.h" + +TEST test_generate_backtrace(void) { + char backtrace[max_stack_size]; + const bool was_successful = generate_backtrace(backtrace, 0, max_stack_size); + std::list backtrace_lines = testutils::split_backtrace_into_lines( + backtrace); + unsigned int lines_count = backtrace_lines.size(); + ASSERT(was_successful); + const char *regex = "(\\d+)(.*(? 0 && lines_count <= max_stack_frames); + for (auto it = backtrace_lines.begin(); it != backtrace_lines.end(); ++it) { + ASSERT(std::regex_match(it->c_str(), std::regex(regex))); + } + PASS(); +} + +TEST test_generate_backtrace_starts_from_the_given_frame_index(void) { + const size_t fake_start_frame_index = 2; + const size_t fake_max_stack_size = 5; + char backtrace[max_stack_size]; + const bool was_successful = generate_backtrace(backtrace, + fake_start_frame_index, + fake_max_stack_size); + std::list backtrace_lines = testutils::split_backtrace_into_lines( + backtrace); + unsigned int lines_count = backtrace_lines.size(); + ASSERT(was_successful); + ASSERT(lines_count == (fake_max_stack_size - fake_start_frame_index + 1)); + PASS(); +} + +TEST test_generate_backtrace_will_return_false_if_size_is_exceeded(void) { + const size_t backtrace_size = 1; + char backtrace[backtrace_size]; + const bool was_successful = generate_backtrace(backtrace, 0, backtrace_size); + ASSERT_FALSE(was_successful); + PASS(); +} + +TEST test_generate_backtrace_will_return_truncated_string_if_size_is_exceeded(void) { + const size_t backtrace_size = 3; + char backtrace[backtrace_size]; + const bool was_successful = generate_backtrace(backtrace, 0, backtrace_size); + ASSERT_FALSE(was_successful); + ASSERT(backtrace[0] != '\0'); + ASSERT(backtrace[1] != '\0'); + PASS(); +} + + +SUITE (backtrace_generation) { + RUN_TEST(test_generate_backtrace); + RUN_TEST(test_generate_backtrace_will_return_false_if_size_is_exceeded); + RUN_TEST(test_generate_backtrace_will_return_truncated_string_if_size_is_exceeded); + RUN_TEST(test_generate_backtrace_starts_from_the_given_frame_index); +} diff --git a/features/dd-sdk-android-ndk/src/test/cpp/test-signal-monitor.cpp b/features/dd-sdk-android-ndk/src/test/cpp/test-signal-monitor.cpp new file mode 100644 index 0000000000..cdce38605a --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/test-signal-monitor.cpp @@ -0,0 +1,150 @@ +#include +#include +#include + +#include "greatest/greatest.h" +#include "utils/signal-monitor.h" + +extern bool handlers_installed; +extern struct sigaction *original_sigactions; + + +#ifndef NDEBUG +extern int performed_install_ops; +extern int performed_uninstall_ops; +#endif + +void clear_tests() { +#ifndef NDEBUG + // reset the signal_monitor state + stop_monitoring(); + performed_install_ops = 0; + performed_uninstall_ops = 0; +#endif +} + +bool performedUninstall(int times) { +#ifndef NDEBUG + return performed_uninstall_ops == times; +#else + return true; +#endif +} + +bool performedInstall(int times) { +#ifndef NDEBUG + return performed_install_ops == times; +#else + return true; +#endif +} + + +TEST calling_start_monitoring_will_install_signal_handlers(void) { + start_monitoring(); + ASSERT(handlers_installed); + clear_tests(); + PASS(); +} + +TEST calling_start_monitoring_more_times_in_a_row_will_only_install_once(void) { + start_monitoring(); + start_monitoring(); + start_monitoring(); + ASSERT(handlers_installed); + ASSERT(performedInstall(1)); + clear_tests(); + PASS(); +} + + +TEST calling_stop_monitoring_will_uninstall_the_handlers(void) { + // given + start_monitoring(); + // when + stop_monitoring(); + ASSERT_FALSE(handlers_installed); + clear_tests(); + PASS(); +} + +TEST calling_stop_monitoring_more_times_in_a_row_will_only_uninstall_once(void) { + // given + start_monitoring(); + // when + stop_monitoring(); + stop_monitoring(); + stop_monitoring(); + ASSERT_FALSE(handlers_installed); + ASSERT(performedUninstall(1)); + clear_tests(); + PASS(); +} + +TEST calling_start_monitor_from_different_threads_will_only_install_once(void) { + auto start_monitoring_lambda = [] { + start_monitoring(); + }; + std::thread t1 = std::thread(start_monitoring_lambda); + std::thread t2 = std::thread(start_monitoring_lambda); + t1.join(); + t2.join(); + ASSERT(handlers_installed); + ASSERT(performedInstall(1)); + clear_tests(); + PASS(); +} + +TEST calling_start_monitor_from_different_threads_will_not_corrupt_the_memory(void) { + auto start_monitoring_lambda = [] { + start_monitoring(); + }; + std::thread t1 = std::thread(start_monitoring_lambda); + std::thread t2 = std::thread(start_monitoring_lambda); + t1.join(); + t2.join(); + // as the watchdog reference will change here we need to make sure we are waiting for both + // to finish so we will sleep for 5 seconds + sleep(5); + int sigactions_size = 0; + const int handled_signals = 6; + struct sigaction *end_pointer = original_sigactions + handled_signals; + struct sigaction *pointer = original_sigactions; + while (pointer != end_pointer && pointer != nullptr) { + sigactions_size++; + pointer++; + } + ASSERT(handlers_installed); + ASSERT(performedInstall(1)); + ASSERT_EQ(sigactions_size, handled_signals); + clear_tests(); + PASS(); +} + +TEST calling_stop_monitoring_from_2_different_threads_will_only_uninstall_once(void) { + // given + start_monitoring(); + // when + auto stop_monitoring_lambda = [] { + stop_monitoring(); + }; + std::thread t1 = std::thread(stop_monitoring_lambda); + std::thread t2 = std::thread(stop_monitoring_lambda); + t1.join(); + t2.join(); + ASSERT_FALSE(handlers_installed); + ASSERT(performedUninstall(1)); + clear_tests(); + PASS(); +} + + +SUITE (signal_monitor) { + RUN_TEST(calling_start_monitor_from_different_threads_will_only_install_once); + RUN_TEST(calling_start_monitor_from_different_threads_will_not_corrupt_the_memory); + RUN_TEST(calling_stop_monitoring_from_2_different_threads_will_only_uninstall_once); + RUN_TEST(calling_start_monitoring_will_install_signal_handlers); + RUN_TEST(calling_start_monitoring_more_times_in_a_row_will_only_install_once); + RUN_TEST(calling_stop_monitoring_will_uninstall_the_handlers); + RUN_TEST(calling_stop_monitoring_more_times_in_a_row_will_only_uninstall_once); +} diff --git a/features/dd-sdk-android-ndk/src/test/cpp/test-utils.cpp b/features/dd-sdk-android-ndk/src/test/cpp/test-utils.cpp new file mode 100644 index 0000000000..2cb6e299b3 --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/cpp/test-utils.cpp @@ -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 2016-Present Datadog, Inc. + */ + + +#include "test-utils.h" + +#include +#include +#include + +namespace testutils { + + std::string substr(char *source, size_t size) { + char substr[size]; + strncpy(substr, source, size); + return std::string(substr); + } + + std::list split_backtrace_into_lines(const char *buffer) { + std::list to_return; + char *current_address = (char *) buffer; + int substr_length = 0; + char *start_substr_buffer = (char *) current_address; + for (char c = *current_address; c; c = *++current_address) { + // cannot match a single \n character as we specifically + // add them as separated escaped characters in the backtrace to match the Java one + if (c == 'n' && *(current_address - 1) == '\\') { + const std::string x = substr(start_substr_buffer, substr_length - 2); + to_return.push_back(x); + // we want to skip the 'n' character next time we do the substring + start_substr_buffer = (char *) (current_address + 1); + substr_length = 0; + } + substr_length++; + } + // add the last line if there was no split character at the end + if (start_substr_buffer < current_address) { + to_return.push_front(substr(start_substr_buffer, substr_length)); + } + + return to_return; + } + +} \ No newline at end of file diff --git a/dd-sdk-android-ndk/src/test/cpp/test_utils.h b/features/dd-sdk-android-ndk/src/test/cpp/test-utils.h similarity index 100% rename from dd-sdk-android-ndk/src/test/cpp/test_utils.h rename to features/dd-sdk-android-ndk/src/test/cpp/test-utils.h diff --git a/dd-sdk-android-ndk/src/test/cpp/utils/greatest/greatest.h b/features/dd-sdk-android-ndk/src/test/cpp/utils/greatest/greatest.h similarity index 99% rename from dd-sdk-android-ndk/src/test/cpp/utils/greatest/greatest.h rename to features/dd-sdk-android-ndk/src/test/cpp/utils/greatest/greatest.h index 1209c0510f..06353e2538 100644 --- a/dd-sdk-android-ndk/src/test/cpp/utils/greatest/greatest.h +++ b/features/dd-sdk-android-ndk/src/test/cpp/utils/greatest/greatest.h @@ -125,7 +125,7 @@ int main(int argc, char **argv) { /* Make it possible to replace fprintf with another * function with the same interface. */ #ifndef GREATEST_FPRINTF -#define GREATEST_FPRINTF fprintf +#define GREATEST_FPRINTF(ignore, fmt, ...) __android_log_print(ANDROID_LOG_INFO, "DatadogNDKTests", fmt, ##__VA_ARGS__) #endif #if GREATEST_USE_LONGJMP diff --git a/features/dd-sdk-android-ndk/src/test/kotlin/com/datadog/android/ndk/NdkCrashReportsTest.kt b/features/dd-sdk-android-ndk/src/test/kotlin/com/datadog/android/ndk/NdkCrashReportsTest.kt new file mode 100644 index 0000000000..8592ae2fde --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/kotlin/com/datadog/android/ndk/NdkCrashReportsTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk + +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.ndk.internal.NdkCrashReportsFeature +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class NdkCrashReportsTest { + + @Mock + lateinit var mockSdkCore: FeatureSdkCore + + @BeforeEach + fun `set up`() { + whenever(mockSdkCore.internalLogger) doReturn mock() + } + + @Test + fun `M register ndk crash reports feature W enable()`() { + // When + NdkCrashReports.enable(mockSdkCore) + + // Then + argumentCaptor { + verify(mockSdkCore).registerFeature(capture()) + } + } +} diff --git a/features/dd-sdk-android-ndk/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeatureTest.kt b/features/dd-sdk-android-ndk/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeatureTest.kt new file mode 100644 index 0000000000..330686958c --- /dev/null +++ b/features/dd-sdk-android-ndk/src/test/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeatureTest.kt @@ -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 2016-Present Datadog, Inc. + */ + +package com.datadog.android.ndk.internal + +import android.content.Context +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.privacy.TrackingConsent +import com.datadog.tools.unit.setFieldValue +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +class NdkCrashReportsFeatureTest { + + private lateinit var testedFeature: NdkCrashReportsFeature + + @Mock + lateinit var mockSdkCore: InternalSdkCore + + @TempDir + lateinit var tempDir: File + + @BeforeEach + fun `set up`() { + testedFeature = NdkCrashReportsFeature(mockSdkCore) + } + + @Test + fun `M resolve to PENDING int state W consentToInt { PENDING }`() { + assertThat(testedFeature.consentToInt(TrackingConsent.PENDING)).isEqualTo( + NdkCrashReportsFeature.TRACKING_CONSENT_PENDING + ) + } + + @Test + fun `M resolve to GRANTED int state W consentToInt { GRANTED }`() { + assertThat(testedFeature.consentToInt(TrackingConsent.GRANTED)).isEqualTo( + NdkCrashReportsFeature.TRACKING_CONSENT_GRANTED + ) + } + + @Test + fun `M resolve to NOT_GRANTED int state W consentToInt { NOT_GRANTED }`() { + assertThat(testedFeature.consentToInt(TrackingConsent.NOT_GRANTED)).isEqualTo( + NdkCrashReportsFeature.TRACKING_CONSENT_NOT_GRANTED + ) + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class) + fun `M create the NDK crash reports directory W onInitialize { nativeLibrary loaded }`( + trackingConsent: TrackingConsent + ) { + // GIVEN + val mockContext: Context = mock() + whenever(mockSdkCore.rootStorageDir) doReturn tempDir + whenever(mockSdkCore.trackingConsent) doReturn trackingConsent + whenever(mockSdkCore.internalLogger) doReturn mock() + testedFeature.setFieldValue("nativeLibraryLoaded", true) + + // WHEN + try { + testedFeature.onInitialize(mockContext) + } catch (e: UnsatisfiedLinkError) { + // Do nothing. Just to avoid the NDK linkage error. + } + + // THEN + val ndkCrashDirectory = File(tempDir, NdkCrashReportsFeature.NDK_CRASH_REPORTS_FOLDER) + assertThat(ndkCrashDirectory.exists()).isTrue() + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class) + fun `M do nothing W register { nativeLibrary not loaded }`( + trackingConsent: TrackingConsent + ) { + // GIVEN + val mockContext: Context = mock() + whenever(mockSdkCore.rootStorageDir) doReturn tempDir + whenever(mockSdkCore.trackingConsent) doReturn trackingConsent + whenever(mockSdkCore.internalLogger) doReturn mock() + + // WHEN + try { + testedFeature.onInitialize(mockContext) + } catch (e: UnsatisfiedLinkError) { + // Do nothing. Just to avoid the NDK linkage error. + } + + // THEN + val ndkCrashDirectory = File(tempDir, NdkCrashReportsFeature.NDK_CRASH_REPORTS_FOLDER) + assertThat(ndkCrashDirectory.exists()).isFalse() + } + + @ParameterizedTest + @EnumSource(TrackingConsent::class) + fun `M do nothing W register { no root storage dir }`( + trackingConsent: TrackingConsent + ) { + // GIVEN + val mockContext: Context = mock() + whenever(mockSdkCore.rootStorageDir) doReturn null + whenever(mockSdkCore.trackingConsent) doReturn trackingConsent + val mockInternalLogger = mock() + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + testedFeature.setFieldValue("nativeLibraryLoaded", true) + + // WHEN + try { + testedFeature.onInitialize(mockContext) + } catch (e: UnsatisfiedLinkError) { + // Do nothing. Just to avoid the NDK linkage error. + } + + // THEN + val ndkCrashDirectory = File(tempDir, NdkCrashReportsFeature.NDK_CRASH_REPORTS_FOLDER) + assertThat(ndkCrashDirectory.exists()).isFalse() + argumentCaptor<() -> String> { + verify(mockInternalLogger) + .log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + isNull(), + eq(false), + eq(null) + ) + assertThat(lastValue()).isEqualTo(NdkCrashReportsFeature.NO_SDK_ROOT_DIR_MESSAGE) + } + } +} diff --git a/features/dd-sdk-android-ndk/transitiveDependencies b/features/dd-sdk-android-ndk/transitiveDependencies new file mode 100644 index 0000000000..17855d8899 --- /dev/null +++ b/features/dd-sdk-android-ndk/transitiveDependencies @@ -0,0 +1,12 @@ +Dependencies List + +androidx.multidex:multidex:2.0.1 : 26 Kb +com.squareup.okhttp3:okhttp:4.12.0 : 771 Kb +com.squareup.okio:okio-jvm:3.6.0 : 351 Kb +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 : 959 b +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 : 965 b +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 : 1706 Kb +org.jetbrains:annotations:13.0 : 17 Kb + +Total transitive dependencies size : 2 Mb + diff --git a/features/dd-sdk-android-rum/.gitignore b/features/dd-sdk-android-rum/.gitignore new file mode 100644 index 0000000000..e5614b9871 --- /dev/null +++ b/features/dd-sdk-android-rum/.gitignore @@ -0,0 +1,18 @@ +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +build/ diff --git a/features/dd-sdk-android-rum/README.md b/features/dd-sdk-android-rum/README.md new file mode 100644 index 0000000000..5a00f30935 --- /dev/null +++ b/features/dd-sdk-android-rum/README.md @@ -0,0 +1,5 @@ +# Datadog RUM SDK for Android + +See the dedicated [Datadog Android RUM Collection documentation][1] to learn how to send RUM data from your Android or Android TV application to Datadog. + +[1]: https://docs.datadoghq.com/real_user_monitoring/android/?tab=kotlin \ No newline at end of file diff --git a/features/dd-sdk-android-rum/api/apiSurface b/features/dd-sdk-android-rum/api/apiSurface new file mode 100644 index 0000000000..adfef60d54 --- /dev/null +++ b/features/dd-sdk-android-rum/api/apiSurface @@ -0,0 +1,2779 @@ +fun T.useMonitored(com.datadog.android.api.SdkCore = Datadog.getInstance(), (T) -> R): R +annotation com.datadog.android.rum.ExperimentalRumApi +object com.datadog.android.rum.GlobalRumMonitor + fun isRegistered(com.datadog.android.api.SdkCore = Datadog.getInstance()): Boolean + fun get(com.datadog.android.api.SdkCore = Datadog.getInstance()): RumMonitor +object com.datadog.android.rum.Rum + fun enable(RumConfiguration, com.datadog.android.api.SdkCore = Datadog.getInstance()) +enum com.datadog.android.rum.RumActionType + - TAP + - SCROLL + - SWIPE + - CLICK + - BACK + - CUSTOM +object com.datadog.android.rum.RumAttributes + const val APPLICATION_VERSION: String + const val ENV: String + const val SERVICE_NAME: String + const val SOURCE: String + const val VARIANT: String + const val SDK_VERSION: String + const val INTERNAL_ERROR_TYPE: String + const val INTERNAL_TIMESTAMP: String + const val INTERNAL_ERROR_SOURCE_TYPE: String + const val INTERNAL_ERROR_IS_CRASH: String + const val TRACE_ID: String + const val SPAN_ID: String + const val RULE_PSR: String + const val RESOURCE_TIMINGS: String + const val GRAPHQL_OPERATION_TYPE: String + const val GRAPHQL_OPERATION_NAME: String + const val GRAPHQL_PAYLOAD: String + const val GRAPHQL_VARIABLES: String + const val ERROR_RESOURCE_METHOD: String + const val ERROR_RESOURCE_STATUS_CODE: String + const val ERROR_RESOURCE_URL: String + const val ERROR_DATABASE_VERSION: String + const val ERROR_DATABASE_PATH: String + const val ERROR_FINGERPRINT: String + const val ACTION_TARGET_CLASS_NAME: String + const val ACTION_TARGET_TITLE: String + const val ACTION_TARGET_PARENT_INDEX: String + const val ACTION_TARGET_PARENT_CLASSNAME: String + const val ACTION_TARGET_PARENT_RESOURCE_ID: String + const val ACTION_TARGET_RESOURCE_ID: String + const val ACTION_TARGET_SELECTED: String + const val ACTION_TARGET_ROLE: String + const val ACTION_GESTURE_DIRECTION: String + const val ACTION_GESTURE_FROM_STATE: String + const val ACTION_GESTURE_TO_STATE: String + const val LONG_TASK_TARGET: String + const val NETWORK_CARRIER_ID: String + const val NETWORK_CARRIER_NAME: String + const val NETWORK_CONNECTIVITY: String + const val NETWORK_DOWN_KBPS: String + const val NETWORK_SIGNAL_STRENGTH: String + const val NETWORK_UP_KBPS: String + const val NETWORK_BYTES_READ: String + const val FLUTTER_FIRST_BUILD_COMPLETE: String + const val CUSTOM_INV_VALUE: String +data class com.datadog.android.rum.RumConfiguration + class Builder + constructor(String) + fun setSessionSampleRate(Float): Builder + fun collectAccessibility(Boolean): Builder + fun setTelemetrySampleRate(Float): Builder + fun trackUserInteractions(Array = emptyArray(), com.datadog.android.rum.tracking.InteractionPredicate = NoOpInteractionPredicate()): Builder + fun disableUserInteractionTracking(): Builder + fun useViewTrackingStrategy(com.datadog.android.rum.tracking.ViewTrackingStrategy?): Builder + fun trackLongTasks(Long = RumFeature.DEFAULT_LONG_TASK_THRESHOLD_MS): Builder + fun trackNonFatalAnrs(Boolean): Builder + fun setViewEventMapper(com.datadog.android.rum.event.ViewEventMapper): Builder + fun setResourceEventMapper(com.datadog.android.event.EventMapper): Builder + fun setActionEventMapper(com.datadog.android.event.EventMapper): Builder + fun setErrorEventMapper(com.datadog.android.event.EventMapper): Builder + fun setLongTaskEventMapper(com.datadog.android.event.EventMapper): Builder + fun setVitalEventMapper(com.datadog.android.event.EventMapper): Builder + fun trackBackgroundEvents(Boolean): Builder + fun trackFrustrations(Boolean): Builder + fun setVitalsUpdateFrequency(com.datadog.android.rum.configuration.VitalsUpdateFrequency): Builder + fun useCustomEndpoint(String): Builder + fun setSessionListener(RumSessionListener): Builder + fun setInitialResourceIdentifier(com.datadog.android.rum.metric.networksettled.InitialResourceIdentifier): Builder + fun setLastInteractionIdentifier(com.datadog.android.rum.metric.interactiontonextview.LastInteractionIdentifier?): Builder + fun setSlowFramesConfiguration(com.datadog.android.rum.configuration.SlowFramesConfiguration?): Builder + fun trackAnonymousUser(Boolean): Builder + fun build(): RumConfiguration +enum com.datadog.android.rum.RumErrorSource + - NETWORK + - SOURCE + - CONSOLE + - LOGGER + - AGENT + - WEBVIEW + - CUSTOM + - REPORT +interface com.datadog.android.rum.RumMonitor + fun getCurrentSessionId((String?) -> Unit) + fun startView(Any, String, Map = emptyMap()) + fun stopView(Any, Map = emptyMap()) + fun addAction(RumActionType, String, Map = emptyMap()) + fun startAction(RumActionType, String, Map = emptyMap()) + fun stopAction(RumActionType, String, Map = emptyMap()) + fun startResource(String, RumResourceMethod, String, Map = emptyMap()) + fun stopResource(String, Int?, Long?, RumResourceKind, Map = emptyMap()) + fun stopResourceWithError(String, Int?, String, RumErrorSource, Throwable, Map = emptyMap()) + fun stopResourceWithError(String, Int?, String, RumErrorSource, String, String?, Map = emptyMap()) + fun addError(String, RumErrorSource, Throwable?, Map = emptyMap()) + fun addErrorWithStacktrace(String, RumErrorSource, String?, Map = emptyMap()) + fun addTiming(String) + fun addFeatureFlagEvaluation(String, Any) + fun addFeatureFlagEvaluations(Map) + fun addAttribute(String, Any?) + fun removeAttribute(String) + fun getAttributes(): Map + fun clearAttributes() + fun stopSession() + fun addViewLoadingTime(Boolean) + fun addViewAttributes(Map) + fun removeViewAttributes(Collection) + fun startFeatureOperation(String, String? = null, Map = emptyMap()) + fun succeedFeatureOperation(String, String? = null, Map = emptyMap()) + fun failFeatureOperation(String, String? = null, com.datadog.android.rum.featureoperations.FailureReason, Map = emptyMap()) + var debug: Boolean + fun _getInternal(): _RumInternalProxy? +enum com.datadog.android.rum.RumPerformanceMetric + - FLUTTER_BUILD_TIME + - FLUTTER_RASTER_TIME + - JS_FRAME_TIME +interface com.datadog.android.rum.RumResourceAttributesProvider + fun onProvideAttributes(okhttp3.Request, okhttp3.Response?, Throwable?): Map +enum com.datadog.android.rum.RumResourceKind + constructor(String) + - BEACON + - FETCH + - XHR + - DOCUMENT + - NATIVE + - UNKNOWN + - IMAGE + - JS + - FONT + - CSS + - MEDIA + - OTHER + companion object + fun fromMimeType(String): RumResourceKind +enum com.datadog.android.rum.RumResourceMethod + - POST + - GET + - HEAD + - PUT + - DELETE + - PATCH + - TRACE + - OPTIONS + - CONNECT +interface com.datadog.android.rum.RumSessionListener + fun onSessionStarted(String, Boolean) +enum com.datadog.android.rum.RumSessionType + - SYNTHETICS + - USER +class com.datadog.android.rum._RumInternalProxy + fun addLongTask(Long, String) + fun updatePerformanceMetric(RumPerformanceMetric, Double) + fun updateExternalRefreshRate(Double) + fun setInternalViewAttribute(String, Any?) + fun setSyntheticsAttribute(String?, String?) + fun enableJankStatsTracking(android.app.Activity) + companion object + fun setTelemetryConfigurationEventMapper(com.datadog.android.rum.RumConfiguration.Builder, com.datadog.android.event.EventMapper): com.datadog.android.rum.RumConfiguration.Builder + fun setAdditionalConfiguration(com.datadog.android.rum.RumConfiguration.Builder, Map): com.datadog.android.rum.RumConfiguration.Builder + fun setComposeActionTrackingStrategy(com.datadog.android.rum.RumConfiguration.Builder, com.datadog.android.rum.tracking.ActionTrackingStrategy): com.datadog.android.rum.RumConfiguration.Builder + fun setRumSessionTypeOverride(com.datadog.android.rum.RumConfiguration.Builder, RumSessionType): com.datadog.android.rum.RumConfiguration.Builder +data class com.datadog.android.rum.configuration.SlowFramesConfiguration + constructor(Int = DEFAULT_SLOW_FRAME_RECORDS_MAX_AMOUNT, Long = DEFAULT_FROZEN_FRAME_THRESHOLD_NS, Long = DEFAULT_CONTINUOUS_SLOW_FRAME_THRESHOLD_NS, Long = DEFAULT_FREEZE_DURATION_NS, Long = DEFAULT_VIEW_LIFETIME_THRESHOLD_NS) + companion object + val DEFAULT: SlowFramesConfiguration +enum com.datadog.android.rum.configuration.VitalsUpdateFrequency + constructor(Long) + - FREQUENT + - AVERAGE + - RARE + - NEVER +interface com.datadog.android.rum.event.ViewEventMapper : com.datadog.android.event.EventMapper + override fun map(com.datadog.android.rum.model.ViewEvent): com.datadog.android.rum.model.ViewEvent +enum com.datadog.android.rum.featureoperations.FailureReason + - ERROR + - ABANDONED + - OTHER +data class com.datadog.android.rum.internal.domain.event.ResourceTiming + constructor(Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L) +interface com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor : com.datadog.android.rum.RumMonitor + fun waitForResourceTiming(Any) + fun addResourceTiming(Any, com.datadog.android.rum.internal.domain.event.ResourceTiming) + fun notifyInterceptorInstantiated() + fun startResource(com.datadog.android.rum.resource.ResourceId, com.datadog.android.rum.RumResourceMethod, String, Map = emptyMap()) + fun stopResource(com.datadog.android.rum.resource.ResourceId, Int?, Long?, com.datadog.android.rum.RumResourceKind, Map = emptyMap()) + fun stopResourceWithError(com.datadog.android.rum.resource.ResourceId, Int?, String, com.datadog.android.rum.RumErrorSource, Throwable, Map = emptyMap()) + fun stopResourceWithError(com.datadog.android.rum.resource.ResourceId, Int?, String, com.datadog.android.rum.RumErrorSource, String, String?, Map = emptyMap()) +interface com.datadog.android.rum.metric.interactiontonextview.LastInteractionIdentifier + fun validate(PreviousViewLastInteractionContext): Boolean +data class com.datadog.android.rum.metric.interactiontonextview.PreviousViewLastInteractionContext + constructor(com.datadog.android.rum.model.ActionEvent.ActionEventActionType, Long, Long?) +class com.datadog.android.rum.metric.interactiontonextview.TimeBasedInteractionIdentifier : LastInteractionIdentifier + constructor(Long = DEFAULT_TIME_THRESHOLD_MS) + override fun validate(PreviousViewLastInteractionContext): Boolean + override fun equals(Any?): Boolean + override fun hashCode(): Int + companion object +interface com.datadog.android.rum.metric.networksettled.InitialResourceIdentifier + fun validate(NetworkSettledResourceContext): Boolean +data class com.datadog.android.rum.metric.networksettled.NetworkSettledResourceContext + constructor(String, Long, Long?) +class com.datadog.android.rum.metric.networksettled.TimeBasedInitialResourceIdentifier : InitialResourceIdentifier + constructor(Long = DEFAULT_TIME_THRESHOLD_MS) + override fun validate(NetworkSettledResourceContext): Boolean + override fun equals(Any?): Boolean + override fun hashCode(): Int + companion object +fun android.content.Context.getAssetAsRumResource(String, Int = AssetManager.ACCESS_STREAMING, com.datadog.android.api.SdkCore = Datadog.getInstance()): java.io.InputStream +fun android.content.Context.getRawResAsRumResource(Int, com.datadog.android.api.SdkCore = Datadog.getInstance()): java.io.InputStream +fun java.io.InputStream.asRumResource(String, com.datadog.android.api.SdkCore = Datadog.getInstance()): java.io.InputStream +class com.datadog.android.rum.resource.ResourceId + constructor(String, String?) + override fun equals(Any?): Boolean + override fun hashCode(): Int +class com.datadog.android.rum.resource.RumResourceInputStream : java.io.InputStream + constructor(java.io.InputStream, String, com.datadog.android.api.SdkCore = Datadog.getInstance()) + override fun read(): Int + override fun read(ByteArray): Int + override fun read(ByteArray, Int, Int): Int + override fun available(): Int + override fun skip(Long): Long + override fun markSupported(): Boolean + override fun mark(Int) + override fun reset() + override fun close() +open class com.datadog.android.rum.tracking.AcceptAllActivities : ComponentPredicate + override fun accept(android.app.Activity): Boolean + override fun getViewName(android.app.Activity): String? + override fun equals(Any?): Boolean + override fun hashCode(): Int +open class com.datadog.android.rum.tracking.AcceptAllDefaultFragment : ComponentPredicate + override fun accept(android.app.Fragment): Boolean + override fun getViewName(android.app.Fragment): String? + override fun equals(Any?): Boolean + override fun hashCode(): Int +open class com.datadog.android.rum.tracking.AcceptAllNavDestinations : ComponentPredicate + override fun accept(androidx.navigation.NavDestination): Boolean + override fun getViewName(androidx.navigation.NavDestination): String? + override fun equals(Any?): Boolean + override fun hashCode(): Int +open class com.datadog.android.rum.tracking.AcceptAllSupportFragments : ComponentPredicate + override fun accept(androidx.fragment.app.Fragment): Boolean + override fun getViewName(androidx.fragment.app.Fragment): String? + override fun equals(Any?): Boolean + override fun hashCode(): Int +interface com.datadog.android.rum.tracking.ActionTrackingStrategy : TrackingStrategy + fun findTargetForTap(android.view.View, Float, Float): ViewTarget? + fun findTargetForScroll(android.view.View, Float, Float): ViewTarget? +abstract class com.datadog.android.rum.tracking.ActivityLifecycleTrackingStrategy : android.app.Application.ActivityLifecycleCallbacks, TrackingStrategy + protected var sdkCore: com.datadog.android.api.feature.FeatureSdkCore + override fun register(com.datadog.android.api.SdkCore, android.content.Context) + override fun unregister(android.content.Context?) + override fun onActivityPaused(android.app.Activity) + override fun onActivityStarted(android.app.Activity) + override fun onActivityDestroyed(android.app.Activity) + override fun onActivitySaveInstanceState(android.app.Activity, android.os.Bundle) + override fun onActivityStopped(android.app.Activity) + override fun onActivityCreated(android.app.Activity, android.os.Bundle?) + override fun onActivityResumed(android.app.Activity) + protected fun withSdkCore((com.datadog.android.api.feature.FeatureSdkCore) -> T): T? +class com.datadog.android.rum.tracking.ActivityViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy + constructor(Boolean, ComponentPredicate = AcceptAllActivities()) + override fun onActivityResumed(android.app.Activity) + override fun onActivityStopped(android.app.Activity) + override fun equals(Any?): Boolean + override fun hashCode(): Int +fun android.os.Bundle?.convertToRumViewAttributes(): Map +interface com.datadog.android.rum.tracking.ComponentPredicate + fun accept(T): Boolean + fun getViewName(T): String? +class com.datadog.android.rum.tracking.FragmentViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy + constructor(Boolean, ComponentPredicate = AcceptAllSupportFragments(), ComponentPredicate = AcceptAllDefaultFragment()) + override fun onActivityStarted(android.app.Activity) + override fun onActivityStopped(android.app.Activity) + override fun equals(Any?): Boolean + override fun hashCode(): Int +interface com.datadog.android.rum.tracking.InteractionPredicate + fun getTargetName(Any): String? +class com.datadog.android.rum.tracking.MixedViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy + constructor(Boolean, ComponentPredicate = AcceptAllActivities(), ComponentPredicate = AcceptAllSupportFragments(), ComponentPredicate = AcceptAllDefaultFragment()) + override fun register(com.datadog.android.api.SdkCore, android.content.Context) + override fun unregister(android.content.Context?) + override fun equals(Any?): Boolean + override fun hashCode(): Int +class com.datadog.android.rum.tracking.NavigationViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy, androidx.navigation.NavController.OnDestinationChangedListener + constructor(Int, Boolean, ComponentPredicate = AcceptAllNavDestinations()) + override fun onActivityStarted(android.app.Activity) + override fun onActivityStopped(android.app.Activity) + override fun onActivityPaused(android.app.Activity) + override fun onDestinationChanged(androidx.navigation.NavController, androidx.navigation.NavDestination, android.os.Bundle?) + fun startTracking() + fun stopTracking() +interface com.datadog.android.rum.tracking.TrackingStrategy + fun register(com.datadog.android.api.SdkCore, android.content.Context) + fun unregister(android.content.Context?) +interface com.datadog.android.rum.tracking.ViewAttributesProvider + fun extractAttributes(android.view.View, MutableMap) +class com.datadog.android.rum.tracking.ViewTarget + constructor(java.lang.ref.WeakReference = WeakReference(null), Node? = null) + override fun equals(Any?): Boolean + override fun hashCode(): Int +data class com.datadog.android.rum.tracking.Node + constructor(String, Map = mapOf()) +interface com.datadog.android.rum.tracking.ViewTrackingStrategy : TrackingStrategy +class com.datadog.android.sqlite.DatadogDatabaseErrorHandler : android.database.DatabaseErrorHandler + constructor(String? = null, android.database.DatabaseErrorHandler = DefaultDatabaseErrorHandler()) + override fun onCorruption(android.database.sqlite.SQLiteDatabase) +data class com.datadog.android.rum.model.ActionEvent + constructor(kotlin.Long, Application, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, ActionEventSession, ActionEventSource? = null, ActionEventView, Usr? = null, Account? = null, Connectivity? = null, Display? = null, Synthetics? = null, CiTest? = null, Os? = null, Device? = null, Dd, Context? = null, Container? = null, ActionEventAction) + val type: kotlin.String + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEvent + fun fromJsonObject(com.google.gson.JsonObject): ActionEvent + data class Application + constructor(kotlin.String, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Application + fun fromJsonObject(com.google.gson.JsonObject): Application + data class ActionEventSession + constructor(kotlin.String, ActionEventSessionType, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEventSession + fun fromJsonObject(com.google.gson.JsonObject): ActionEventSession + data class ActionEventView + constructor(kotlin.String, kotlin.String? = null, kotlin.String, kotlin.String? = null, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEventView + fun fromJsonObject(com.google.gson.JsonObject): ActionEventView + data class Usr + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Usr + fun fromJsonObject(com.google.gson.JsonObject): Usr + data class Account + constructor(kotlin.String, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Account + fun fromJsonObject(com.google.gson.JsonObject): Account + data class Connectivity + constructor(Status, kotlin.collections.List? = null, EffectiveType? = null, Cellular? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Connectivity + fun fromJsonObject(com.google.gson.JsonObject): Connectivity + data class Display + constructor(Viewport? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Display + fun fromJsonObject(com.google.gson.JsonObject): Display + data class Synthetics + constructor(kotlin.String, kotlin.String, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Synthetics + fun fromJsonObject(com.google.gson.JsonObject): Synthetics + data class CiTest + constructor(kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CiTest + fun fromJsonObject(com.google.gson.JsonObject): CiTest + data class Os + constructor(kotlin.String, kotlin.String, kotlin.String? = null, kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Os + fun fromJsonObject(com.google.gson.JsonObject): Os + data class Device + constructor(DeviceType? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.List? = null, kotlin.String? = null, kotlin.Number? = null, kotlin.Boolean? = null, kotlin.Number? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Device + fun fromJsonObject(com.google.gson.JsonObject): Device + data class Dd + constructor(DdSession? = null, Configuration? = null, kotlin.String? = null, kotlin.String? = null, DdAction? = null) + val formatVersion: kotlin.Long + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Dd + fun fromJsonObject(com.google.gson.JsonObject): Dd + data class Context + constructor(kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Context + fun fromJsonObject(com.google.gson.JsonObject): Context + data class Container + constructor(ContainerView, ActionEventSource) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Container + fun fromJsonObject(com.google.gson.JsonObject): Container + data class ActionEventAction + constructor(ActionEventActionType, kotlin.String? = null, kotlin.Long? = null, ActionEventActionTarget? = null, Frustration? = null, Error? = null, Crash? = null, LongTask? = null, Resource? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEventAction + fun fromJsonObject(com.google.gson.JsonObject): ActionEventAction + data class Cellular + constructor(kotlin.String? = null, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Cellular + fun fromJsonObject(com.google.gson.JsonObject): Cellular + data class Viewport + constructor(kotlin.Number, kotlin.Number) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Viewport + fun fromJsonObject(com.google.gson.JsonObject): Viewport + data class DdSession + constructor(Plan? = null, SessionPrecondition? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): DdSession + fun fromJsonObject(com.google.gson.JsonObject): DdSession + data class Configuration + constructor(kotlin.Number, kotlin.Number? = null, kotlin.Number? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Configuration + fun fromJsonObject(com.google.gson.JsonObject): Configuration + data class DdAction + constructor(Position? = null, DdActionTarget? = null, NameSource? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): DdAction + fun fromJsonObject(com.google.gson.JsonObject): DdAction + data class ContainerView + constructor(kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ContainerView + fun fromJsonObject(com.google.gson.JsonObject): ContainerView + data class ActionEventActionTarget + constructor(kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEventActionTarget + fun fromJsonObject(com.google.gson.JsonObject): ActionEventActionTarget + data class Frustration + constructor(kotlin.collections.List) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Frustration + fun fromJsonObject(com.google.gson.JsonObject): Frustration + data class Error + constructor(kotlin.Long) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Error + fun fromJsonObject(com.google.gson.JsonObject): Error + data class Crash + constructor(kotlin.Long) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Crash + fun fromJsonObject(com.google.gson.JsonObject): Crash + data class LongTask + constructor(kotlin.Long) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): LongTask + fun fromJsonObject(com.google.gson.JsonObject): LongTask + data class Resource + constructor(kotlin.Long) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Resource + fun fromJsonObject(com.google.gson.JsonObject): Resource + data class Position + constructor(kotlin.Long, kotlin.Long) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Position + fun fromJsonObject(com.google.gson.JsonObject): Position + data class DdActionTarget + constructor(kotlin.String? = null, kotlin.Long? = null, kotlin.Long? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): DdActionTarget + fun fromJsonObject(com.google.gson.JsonObject): DdActionTarget + enum ActionEventSource + constructor(kotlin.String) + - ANDROID + - IOS + - BROWSER + - FLUTTER + - REACT_NATIVE + - ROKU + - UNITY + - KOTLIN_MULTIPLATFORM + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEventSource + enum ActionEventSessionType + constructor(kotlin.String) + - USER + - SYNTHETICS + - CI_TEST + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEventSessionType + enum Status + constructor(kotlin.String) + - CONNECTED + - NOT_CONNECTED + - MAYBE + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Status + enum Interface + constructor(kotlin.String) + - BLUETOOTH + - CELLULAR + - ETHERNET + - WIFI + - WIMAX + - MIXED + - OTHER + - UNKNOWN + - NONE + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Interface + enum EffectiveType + constructor(kotlin.String) + - SLOW_2G + - `2G` + - `3G` + - `4G` + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): EffectiveType + enum DeviceType + constructor(kotlin.String) + - MOBILE + - DESKTOP + - TABLET + - TV + - GAMING_CONSOLE + - BOT + - OTHER + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): DeviceType + enum ActionEventActionType + constructor(kotlin.String) + - CUSTOM + - CLICK + - TAP + - SCROLL + - SWIPE + - APPLICATION_START + - BACK + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ActionEventActionType + enum Plan + constructor(kotlin.Number) + - PLAN_1 + - PLAN_2 + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Plan + enum SessionPrecondition + constructor(kotlin.String) + - USER_APP_LAUNCH + - INACTIVITY_TIMEOUT + - MAX_DURATION + - BACKGROUND_LAUNCH + - PREWARM + - FROM_NON_INTERACTIVE_SESSION + - EXPLICIT_STOP + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): SessionPrecondition + enum NameSource + constructor(kotlin.String) + - CUSTOM_ATTRIBUTE + - MASK_PLACEHOLDER + - STANDARD_ATTRIBUTE + - TEXT_CONTENT + - MASK_DISALLOWED + - BLANK + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): NameSource + enum Type + constructor(kotlin.String) + - RAGE_CLICK + - DEAD_CLICK + - ERROR_CLICK + - RAGE_TAP + - ERROR_TAP + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Type +data class com.datadog.android.rum.model.ErrorEvent + constructor(kotlin.Long, Application, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, ErrorEventSession, ErrorEventSource? = null, ErrorEventView, Usr? = null, Account? = null, Connectivity? = null, Display? = null, Synthetics? = null, CiTest? = null, Os? = null, Device? = null, Dd, Context? = null, Action? = null, Container? = null, Error, Freeze? = null, Context? = null) + val type: kotlin.String + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ErrorEvent + fun fromJsonObject(com.google.gson.JsonObject): ErrorEvent + data class Application + constructor(kotlin.String, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Application + fun fromJsonObject(com.google.gson.JsonObject): Application + data class ErrorEventSession + constructor(kotlin.String, ErrorEventSessionType, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ErrorEventSession + fun fromJsonObject(com.google.gson.JsonObject): ErrorEventSession + data class ErrorEventView + constructor(kotlin.String, kotlin.String? = null, kotlin.String, kotlin.String? = null, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ErrorEventView + fun fromJsonObject(com.google.gson.JsonObject): ErrorEventView + data class Usr + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Usr + fun fromJsonObject(com.google.gson.JsonObject): Usr + data class Account + constructor(kotlin.String, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Account + fun fromJsonObject(com.google.gson.JsonObject): Account + data class Connectivity + constructor(Status, kotlin.collections.List? = null, EffectiveType? = null, Cellular? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Connectivity + fun fromJsonObject(com.google.gson.JsonObject): Connectivity + data class Display + constructor(Viewport? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Display + fun fromJsonObject(com.google.gson.JsonObject): Display + data class Synthetics + constructor(kotlin.String, kotlin.String, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Synthetics + fun fromJsonObject(com.google.gson.JsonObject): Synthetics + data class CiTest + constructor(kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CiTest + fun fromJsonObject(com.google.gson.JsonObject): CiTest + data class Os + constructor(kotlin.String, kotlin.String, kotlin.String? = null, kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Os + fun fromJsonObject(com.google.gson.JsonObject): Os + data class Device + constructor(DeviceType? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.List? = null, kotlin.String? = null, kotlin.Number? = null, kotlin.Boolean? = null, kotlin.Number? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Device + fun fromJsonObject(com.google.gson.JsonObject): Device + data class Dd + constructor(DdSession? = null, Configuration? = null, kotlin.String? = null, kotlin.String? = null) + val formatVersion: kotlin.Long + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Dd + fun fromJsonObject(com.google.gson.JsonObject): Dd + data class Context + constructor(kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Context + fun fromJsonObject(com.google.gson.JsonObject): Context + data class Action + constructor(kotlin.collections.List) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Action + fun fromJsonObject(com.google.gson.JsonObject): Action + data class Container + constructor(ContainerView, ErrorEventSource) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Container + fun fromJsonObject(com.google.gson.JsonObject): Container + data class Error + constructor(kotlin.String? = null, kotlin.String, ErrorSource, kotlin.String? = null, kotlin.collections.List? = null, kotlin.Boolean? = null, kotlin.String? = null, kotlin.String? = null, Category? = null, Handling? = null, kotlin.String? = null, SourceType? = null, Resource? = null, kotlin.collections.List? = null, kotlin.collections.List? = null, kotlin.Boolean? = null, Meta? = null, Csp? = null, kotlin.Long? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Error + fun fromJsonObject(com.google.gson.JsonObject): Error + data class Freeze + constructor(kotlin.Long) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Freeze + fun fromJsonObject(com.google.gson.JsonObject): Freeze + data class Cellular + constructor(kotlin.String? = null, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Cellular + fun fromJsonObject(com.google.gson.JsonObject): Cellular + data class Viewport + constructor(kotlin.Number, kotlin.Number) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Viewport + fun fromJsonObject(com.google.gson.JsonObject): Viewport + data class DdSession + constructor(Plan? = null, SessionPrecondition? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): DdSession + fun fromJsonObject(com.google.gson.JsonObject): DdSession + data class Configuration + constructor(kotlin.Number, kotlin.Number? = null, kotlin.Number? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Configuration + fun fromJsonObject(com.google.gson.JsonObject): Configuration + data class ContainerView + constructor(kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ContainerView + fun fromJsonObject(com.google.gson.JsonObject): ContainerView + data class Cause + constructor(kotlin.String, kotlin.String? = null, kotlin.String? = null, ErrorSource) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Cause + fun fromJsonObject(com.google.gson.JsonObject): Cause + data class Resource + constructor(Method, kotlin.Long, kotlin.String, Provider? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Resource + fun fromJsonObject(com.google.gson.JsonObject): Resource + data class Thread + constructor(kotlin.String, kotlin.Boolean, kotlin.String, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Thread + fun fromJsonObject(com.google.gson.JsonObject): Thread + data class BinaryImage + constructor(kotlin.String, kotlin.String, kotlin.Boolean, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): BinaryImage + fun fromJsonObject(com.google.gson.JsonObject): BinaryImage + data class Meta + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Meta + fun fromJsonObject(com.google.gson.JsonObject): Meta + data class Csp + constructor(Disposition? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Csp + fun fromJsonObject(com.google.gson.JsonObject): Csp + data class Provider + constructor(kotlin.String? = null, kotlin.String? = null, ProviderType? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Provider + fun fromJsonObject(com.google.gson.JsonObject): Provider + enum ErrorEventSource + constructor(kotlin.String) + - ANDROID + - IOS + - BROWSER + - FLUTTER + - REACT_NATIVE + - ROKU + - UNITY + - KOTLIN_MULTIPLATFORM + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ErrorEventSource + enum ErrorEventSessionType + constructor(kotlin.String) + - USER + - SYNTHETICS + - CI_TEST + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ErrorEventSessionType + enum Status + constructor(kotlin.String) + - CONNECTED + - NOT_CONNECTED + - MAYBE + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Status + enum Interface + constructor(kotlin.String) + - BLUETOOTH + - CELLULAR + - ETHERNET + - WIFI + - WIMAX + - MIXED + - OTHER + - UNKNOWN + - NONE + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Interface + enum EffectiveType + constructor(kotlin.String) + - SLOW_2G + - `2G` + - `3G` + - `4G` + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): EffectiveType + enum DeviceType + constructor(kotlin.String) + - MOBILE + - DESKTOP + - TABLET + - TV + - GAMING_CONSOLE + - BOT + - OTHER + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): DeviceType + enum ErrorSource + constructor(kotlin.String) + - NETWORK + - SOURCE + - CONSOLE + - LOGGER + - AGENT + - WEBVIEW + - CUSTOM + - REPORT + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ErrorSource + enum Category + constructor(kotlin.String) + - ANR + - APP_HANG + - EXCEPTION + - WATCHDOG_TERMINATION + - MEMORY_WARNING + - NETWORK + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Category + enum Handling + constructor(kotlin.String) + - HANDLED + - UNHANDLED + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Handling + enum SourceType + constructor(kotlin.String) + - ANDROID + - BROWSER + - IOS + - REACT_NATIVE + - FLUTTER + - ROKU + - NDK + - IOS_IL2CPP + - NDK_IL2CPP + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): SourceType + enum Plan + constructor(kotlin.Number) + - PLAN_1 + - PLAN_2 + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Plan + enum SessionPrecondition + constructor(kotlin.String) + - USER_APP_LAUNCH + - INACTIVITY_TIMEOUT + - MAX_DURATION + - BACKGROUND_LAUNCH + - PREWARM + - FROM_NON_INTERACTIVE_SESSION + - EXPLICIT_STOP + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): SessionPrecondition + enum Method + constructor(kotlin.String) + - POST + - GET + - HEAD + - PUT + - DELETE + - PATCH + - TRACE + - OPTIONS + - CONNECT + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Method + enum Disposition + constructor(kotlin.String) + - ENFORCE + - REPORT + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Disposition + enum ProviderType + constructor(kotlin.String) + - AD + - ADVERTISING + - ANALYTICS + - CDN + - CONTENT + - CUSTOMER_SUCCESS + - FIRST_PARTY + - HOSTING + - MARKETING + - OTHER + - SOCIAL + - TAG_MANAGER + - UTILITY + - VIDEO + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ProviderType +data class com.datadog.android.rum.model.LongTaskEvent + constructor(kotlin.Long, Application, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, LongTaskEventSession, LongTaskEventSource? = null, LongTaskEventView, Usr? = null, Account? = null, Connectivity? = null, Display? = null, Synthetics? = null, CiTest? = null, Os? = null, Device? = null, Dd, Context? = null, Action? = null, Container? = null, LongTask) + val type: kotlin.String + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): LongTaskEvent + fun fromJsonObject(com.google.gson.JsonObject): LongTaskEvent + data class Application + constructor(kotlin.String, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Application + fun fromJsonObject(com.google.gson.JsonObject): Application + data class LongTaskEventSession + constructor(kotlin.String, LongTaskEventSessionType, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): LongTaskEventSession + fun fromJsonObject(com.google.gson.JsonObject): LongTaskEventSession + data class LongTaskEventView + constructor(kotlin.String, kotlin.String? = null, kotlin.String, kotlin.String? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): LongTaskEventView + fun fromJsonObject(com.google.gson.JsonObject): LongTaskEventView + data class Usr + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Usr + fun fromJsonObject(com.google.gson.JsonObject): Usr + data class Account + constructor(kotlin.String, kotlin.String? = null, kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Account + fun fromJsonObject(com.google.gson.JsonObject): Account + data class Connectivity + constructor(ConnectivityStatus, kotlin.collections.List? = null, EffectiveType? = null, Cellular? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Connectivity + fun fromJsonObject(com.google.gson.JsonObject): Connectivity + data class Display + constructor(Viewport? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Display + fun fromJsonObject(com.google.gson.JsonObject): Display + data class Synthetics + constructor(kotlin.String, kotlin.String, kotlin.Boolean? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Synthetics + fun fromJsonObject(com.google.gson.JsonObject): Synthetics + data class CiTest + constructor(kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CiTest + fun fromJsonObject(com.google.gson.JsonObject): CiTest + data class Os + constructor(kotlin.String, kotlin.String, kotlin.String? = null, kotlin.String) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Os + fun fromJsonObject(com.google.gson.JsonObject): Os + data class Device + constructor(DeviceType? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.List? = null, kotlin.String? = null, kotlin.Number? = null, kotlin.Boolean? = null, kotlin.Number? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Device + fun fromJsonObject(com.google.gson.JsonObject): Device + data class Dd + constructor(DdSession? = null, Configuration? = null, kotlin.String? = null, kotlin.String? = null, kotlin.Boolean? = null, Profiling? = null) + val formatVersion: kotlin.Long + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Dd + fun fromJsonObject(com.google.gson.JsonObject): Dd + data class Context + constructor(kotlin.collections.MutableMap = mutableMapOf()) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Context + fun fromJsonObject(com.google.gson.JsonObject): Context + data class Action + constructor(kotlin.collections.List) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Action + fun fromJsonObject(com.google.gson.JsonObject): Action + data class Container + constructor(ContainerView, LongTaskEventSource) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Container + fun fromJsonObject(com.google.gson.JsonObject): Container + data class LongTask + constructor(kotlin.String? = null, kotlin.Number? = null, EntryType? = null, kotlin.Long, kotlin.Long? = null, kotlin.Number? = null, kotlin.Number? = null, kotlin.Number? = null, kotlin.Boolean? = null, kotlin.collections.List