diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..c47c4c3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Google Data Transport + url: https://github.com/firebase/firebase-ios-sdk/issues/new/choose + about: Google Data Transport is not supported for direct usage. Please file with the product library that depends on Google Data Transport. diff --git a/.github/ISSUE_TEMPLATE/issue_report.md b/.github/ISSUE_TEMPLATE/issue_report.md new file mode 100644 index 00000000..82769f38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_report.md @@ -0,0 +1,8 @@ +--- +name: 🐞 Internal Issue +about: Googlers may file issues here or to track with [Firebase](https://github.com/firebase/firebase-ios-sdk/issues/new/choose) +--- + \ No newline at end of file diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7ff802fd..ffbe144d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -5,20 +5,27 @@ on: push: branches: main +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: check: runs-on: macos-latest env: MINT_PATH: ${{ github.workspace }}/mint steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: - python-version: 3.6 + python-version: '3.10' - name: Cache Mint packages - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ${{ env.MINT_PATH }} key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }} @@ -28,11 +35,7 @@ jobs: run: ./setup-scripts.sh - name: Setup check - run: | - brew update - brew install clang-format@11 - brew install mint - mint bootstrap + run: scripts/setup_check.sh - name: Style run: scripts/style.sh test-only @@ -46,8 +49,5 @@ jobs: - name: Copyrights run: scripts/check_copyright.sh - - name: Module imports - run: scripts/check_no_module_imports.sh - - name: Imports run: scripts/check_imports.swift diff --git a/.github/workflows/datatransport.yml b/.github/workflows/datatransport.yml index 52f4a609..d704680d 100644 --- a/.github/workflows/datatransport.yml +++ b/.github/workflows/datatransport.yml @@ -6,26 +6,39 @@ on: # Run every day at 11pm (PST) - cron uses UTC times - cron: '0 7 * * *' +env: + # Used to compile test-specific code in the SDK. + GDT_TEST: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: pod_lib_lint: runs-on: macos-latest strategy: matrix: - target: [ios, tvos, macos, watchos] + target: [ios, tvos, macos] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314 - name: Setup Scripts Directory run: ./setup-scripts.sh - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: PodLibLint DataTransport run: | - scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=${{ matrix.target }} + scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=${{ matrix.target }} --analyze catalyst: runs-on: macOS-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314 - name: Setup Scripts Directory run: ./setup-scripts.sh - name: Setup Bundler @@ -33,37 +46,39 @@ jobs: - name: Setup project and Test Catalyst run: scripts/third_party/travis/retry.sh scripts/test_catalyst.sh GoogleDataTransport build - watchos-testapp: - runs-on: macOS-latest - steps: - - uses: actions/checkout@v2 - - name: Xcode 12 - run: sudo xcode-select -s /Applications/Xcode_12.app/Contents/Developer - - name: Setup Scripts Directory - run: ./setup-scripts.sh - - name: Setup Bundler - run: scripts/setup_bundler.sh - - name: Prereqs - run: scripts/install_prereqs.sh GoogleDataTransport watchOS xcodebuild - - name: Setup project and build watchOS test app - run: scripts/third_party/travis/retry.sh scripts/build.sh GoogleDataTransport watchOS xcodebuild +# TODO: Investigate how to build correctly after build failure introduced by +# https://github.com/firebase/firebase-ios-sdk/pull/12966 + # watchos-testapp: + # runs-on: macos-12 + # steps: + # - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + # - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314 + # - name: Setup Scripts Directory + # run: ./setup-scripts.sh + # - name: Setup Bundler + # run: scripts/setup_bundler.sh + # - name: Prereqs + # run: scripts/install_prereqs.sh GoogleDataTransport watchOS xcodebuild + # - name: Setup project and build watchOS test app + # run: scripts/third_party/travis/retry.sh scripts/build.sh GoogleDataTransport watchOS xcodebuild datatransport-options-matrix: runs-on: macos-latest strategy: matrix: - target: [ios, tvos, macos, watchos] + target: [ios, tvos, macos] flags: [ '--use-static-frameworks', '--use-libraries' ] needs: pod_lib_lint steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314 - name: Setup Scripts Directory run: ./setup-scripts.sh - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: PodLibLint DataTransport Cron run: | - scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=${{ matrix.target }} ${{ matrix.flags }} + scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=${{ matrix.target }} ${{ matrix.flags }} --analyze diff --git a/.github/workflows/spm.yml b/.github/workflows/spm.yml index 2fe8365a..7c0e2f69 100644 --- a/.github/workflows/spm.yml +++ b/.github/workflows/spm.yml @@ -6,16 +6,26 @@ on: # Run every day at 11pm (PST) - cron uses UTC times - cron: '0 7 * * *' +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: swift-build-run: - runs-on: macOS-latest + runs-on: macOS-26 strategy: matrix: target: [iOS, tvOS, macOS, catalyst] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314 - name: Setup Scripts Directory run: ./setup-scripts.sh + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_26.4.app/Contents/Developer - name: Initialize xcodebuild run: xcodebuild -list - name: iOS Unit Tests diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..47b322c9 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.1 diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 00000000..7def2b71 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,14 @@ +# Formatting Options - Mimic Google style +--indent 2 +--maxwidth 100 +--wrapparameters afterfirst + +# Disabled Rules + +# Too many of our swift files have simplistic examples. While technically +# it's correct to remove the unused argument labels, it makes our examples +# look wrong. +--disable unusedArguments + +# We prefer trailing braces. +--disable wrapMultilineStatementBraces diff --git a/GoogleDataTransport/CHANGELOG.md b/CHANGELOG.md similarity index 64% rename from GoogleDataTransport/CHANGELOG.md rename to CHANGELOG.md index fd7a29d2..5da55242 100644 --- a/GoogleDataTransport/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,82 @@ +# Unreleased +- Cancel upload operation when background task expires. +- Log error when handling directory enumeration. + +# v10.1.0 +- Fix `[FBLPromise HTTPBody]` SwiftUI Previews crash when using binary + distribution. ([firebase-ios-sdk/#13318](https://github.com/firebase/firebase-ios-sdk/issues/13318), + [firebase-ios-sdk/#11733](https://github.com/firebase/firebase-ios-sdk/issues/11733)) + +# v10.0.0 +- Fix null pointer warning from profiler. (#124) +- [changed] **Breaking change**: Platform Minimum supported version updates: + - | Platform | GoogleDataTransport 10.0| + | ------------- | ------------- | + | iOS | **12.0** | + | tvOS | **13.0** | + | macOS | **10.15** | + | watchOS | **7.0** | + +# v9.4.1 (CocoaPods Only) +- The v9.4.0 podspec did not include the intended dependency range for nanopb + ([firebase-ios-sdk/#12477](https://github.com/firebase/firebase-ios-sdk/issues/12477)) + +# v9.4.0 +- Add privacy manifest. (#114) + +# v9.3.0 +- Added new APIs for attaching product ID data to an event. +- Fix `0x8BADF00D` crashes and add diagnostics. ([firebase-ios-sdk/#9859](https://github.com/firebase/firebase-ios-sdk/issues/9859)) + +# v9.2.5 +- Replace 'TARGET_OS_XR' with 'TARGET_OS_VISION' for compatibility with + Xcode 15 Beta 5. + +# v9.2.4 (SwiftPM Only) +- Fix build errors on the visionOS platform. (#97) + +# v9.2.3 +- Fix analyzer issues shown by Xcode 14.3. ([firebase-ios-sdk/#11228](https://github.com/firebase/firebase-ios-sdk/issues/11228)) + +# v9.2.2 +- Fix new warnings shown by Xcode 14.3. + +# v9.2.1 +- Add `GDTCORTarget` documentation and use Google domain in testing URL. (#89) + +# v9.2.0 +- Add and integrate `MacClientInfo` proto message. (#57) +- Add metrics component to measure SDK performance. (#77) + +# v9.1.4 +- Rollback logging level changes from v9.1.3. (#60) + +# v9.1.3 +- Update to support both nanopb 0.3.9.8 and 0.3.9.9. (#59) +- GoogleDataTransport now uses the logging level set in GoogleUtilities. (#54) + +# v9.1.2 +- Fix crash in `GDTCCTUploadOperation` on older iOS versions. (#41) + +# v9.1.1 (Swift PM) +- Fix Xcode13b4 Catalyst build (#36) + +# v9.1.0 +- Bump Promises dependency. (#8334) + +# v9.0.1 +- Fix Xcode 12.5 analyze warnings. (#26) + +# v9.0.0 +- Rename `transform:` API to `transformGDTEvent:`. (#19) + +# v8.4.0 +- Update to nanopb 0.3.9.8. (https://github.com/firebase/firebase-ios-sdk/issues/7787) + +# v8.3.1 +- Fix thread sanitizer and undefined behavior diagnostics warnings. ( https://github.com/firebase/firebase-ios-sdk/issues/7771) +- URLSession delegate retain cycle. (https://github.com/firebase/firebase-ios-sdk/issues/7780) + # v8.3.0 - Upload logic was refactored to use [Promises library](https://github.com/google/promises) to improve readability and maintainability of the async code. - Fix some race conditions and edge cases when multiple targets have events ready for upload. diff --git a/Gemfile b/Gemfile index c0e56b84..7dfba34c 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,6 @@ # commit Gemfile and Gemfile.lock. source 'https://rubygems.org' -gem 'cocoapods', '1.10.1' - -gem 'cocoapods-generate', '2.0.1' -gem 'danger', '6.1.0' +gem 'cocoapods', '1.16.2' +gem 'cocoapods-generate', '2.2.5' +gem 'danger', '>= 9.3.1' diff --git a/Gemfile.lock b/Gemfile.lock index ac83864b..9a821ff6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,138 +1,173 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.3) - activesupport (5.2.4.4) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) + CFPropertyList (3.0.8) + activesupport (7.2.3.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1, < 6) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) - claide (1.0.3) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) claide-plugins (0.9.2) cork nap open4 (~> 1.3) - cocoapods (1.10.1) - addressable (~> 2.6) + cocoapods (1.16.2) + addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.10.1) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) - molinillo (~> 0.6.6) + molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (~> 1.4) - xcodeproj (>= 1.19.0, < 2.0) - cocoapods-core (1.10.1) - activesupport (> 5.0, < 6) - addressable (~> 2.6) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) fuzzy_match (~> 2.0.4) nap (~> 1.0) netrc (~> 0.11) - public_suffix + public_suffix (~> 4.0) typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.4) - cocoapods-disable-podfile-validations (0.1.1) - cocoapods-downloader (1.4.0) - cocoapods-generate (2.0.1) - cocoapods-disable-podfile-validations (~> 0.1.1) + cocoapods-deintegrate (1.0.5) + cocoapods-disable-podfile-validations (0.2.0) + cocoapods-downloader (2.1) + cocoapods-generate (2.2.5) + cocoapods-disable-podfile-validations (>= 0.1.1, < 0.3.0) cocoapods-plugins (1.0.0) nap - cocoapods-search (1.0.0) - cocoapods-trunk (1.5.0) + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.1.7) + concurrent-ruby (1.3.7) + connection_pool (3.0.2) cork (0.3.0) colored2 (~> 3.1) - danger (6.1.0) + danger (9.6.0) + base64 (~> 0.2) claide (~> 1.0) claide-plugins (>= 0.9.2) - colored2 (~> 3.1) + colored2 (>= 3.1, < 5) cork (~> 0.1) - faraday (~> 0.9) + faraday (>= 0.9.0, < 3.0) faraday-http-cache (~> 2.0) - git (~> 1.5) - kramdown (~> 2.0) + git (>= 1.13) + kramdown (>= 2.5.1, < 3.0) kramdown-parser-gfm (~> 1.0) - no_proxy_fix - octokit (~> 4.7) - terminal-table (~> 1) + octokit (>= 4.0) + pstore (~> 0.1) + terminal-table (>= 1, < 5) + drb (2.2.3) escape (0.0.4) - ethon (0.12.0) - ffi (>= 1.3.0) - faraday (0.17.0) - multipart-post (>= 1.2, < 3) - faraday-http-cache (2.0.0) - faraday (~> 0.8) - ffi (1.14.2) + ethon (0.18.0) + ffi (>= 1.15.0) + logger + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-http-cache (2.7.0) + faraday (>= 0.8) + faraday-net_http (3.4.4) + net-http (~> 0.5) + ffi (1.17.4) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - git (1.5.0) - httpclient (2.8.3) - i18n (1.8.7) + git (4.3.2) + activesupport (>= 5.0) + addressable (~> 2.8) + process_executer (~> 4.0) + rchardet (~> 1.9) + httpclient (2.9.0) + mutex_m + i18n (1.15.2) concurrent-ruby (~> 1.0) - json (2.5.1) - kramdown (2.3.0) - rexml + json (2.20.0) + kramdown (2.5.2) + rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - minitest (5.14.3) - molinillo (0.6.6) - multipart-post (2.1.1) - nanaimo (0.3.0) + logger (1.7.0) + minitest (5.27.0) + molinillo (0.8.0) + mutex_m (0.3.0) + nanaimo (0.4.0) nap (1.1.0) + net-http (0.9.1) + uri (>= 0.11.1) netrc (0.11.0) - no_proxy_fix (0.1.2) - octokit (4.14.0) - sawyer (~> 0.8.0, >= 0.5.3) + octokit (10.0.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) open4 (1.3.4) - public_suffix (4.0.1) - rexml (3.2.4) - ruby-macho (1.4.0) - sawyer (0.8.2) + process_executer (4.0.4) + track_open_instances (~> 0.1) + pstore (0.2.1) + public_suffix (4.0.7) + rchardet (1.10.2) + rexml (3.4.4) + ruby-macho (2.5.1) + sawyer (0.9.3) addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - thread_safe (0.3.6) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (1.2.9) - thread_safe (~> 0.1) - unicode-display_width (1.6.0) - xcodeproj (1.19.0) + faraday (>= 0.17.3, < 3) + securerandom (0.4.1) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + track_open_instances (0.1.15) + typhoeus (1.6.0) + ethon (>= 0.18.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) PLATFORMS ruby DEPENDENCIES - cocoapods (= 1.10.1) - cocoapods-generate (= 2.0.1) - danger (= 6.1.0) + cocoapods (= 1.16.2) + cocoapods-generate (= 2.2.5) + danger (>= 9.3.1) BUNDLED WITH - 2.1.2 + 2.6.2 diff --git a/GoogleDataTransport.podspec b/GoogleDataTransport.podspec index 68ca0df4..fdbba71e 100644 --- a/GoogleDataTransport.podspec +++ b/GoogleDataTransport.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleDataTransport' - s.version = '8.3.0' + s.version = '10.1.0' s.summary = 'Google iOS SDK data transport.' s.description = <<-DESC @@ -8,17 +8,17 @@ Shared library for iOS SDK data transport needs. DESC s.homepage = 'https://developers.google.com/' - s.license = { :type => 'Apache', :file => 'LICENSE' } + s.license = { :type => 'Apache-2.0', :file => 'LICENSE' } s.authors = 'Google, Inc.' s.source = { :git => 'https://github.com/google/GoogleDataTransport.git', :tag => 'CocoaPods-' + s.version.to_s } - ios_deployment_target = '9.0' - osx_deployment_target = '10.12' - tvos_deployment_target = '10.0' - watchos_deployment_target = '6.0' + ios_deployment_target = '12.0' + osx_deployment_target = '10.15' + tvos_deployment_target = '13.0' + watchos_deployment_target = '7.0' s.ios.deployment_target = ios_deployment_target s.osx.deployment_target = osx_deployment_target @@ -39,25 +39,39 @@ Shared library for iOS SDK data transport needs. s.libraries = ['z'] - s.dependency 'GoogleUtilities/Environment', '~> 7.2' - s.dependency 'nanopb', '~> 2.30907.0' - s.dependency 'PromisesObjC', '~> 1.2' + s.dependency 'nanopb', '~> 3.30910.0' + s.dependency 'PromisesObjC', '~> 2.4' header_search_paths = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}/"' } + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'GoogleDataTransport/Resources/PrivacyInfo.xcprivacy' + } + + # The nanopb pod sets these defs, so we must too. (We *do* require 16bit + # (or larger) fields, so we'd have to set at least PB_FIELD_16BIT + # anyways.) + preprocessor_definitions = + 'PB_FIELD_32BIT=1 PB_NO_PACKED_STRUCTS=1 PB_ENABLE_MALLOC=1 ' \ + 'GDTCOR_VERSION=' + s.version.to_s + + if ENV['GDT_TEST'] && ENV['GDT_TEST'] == '1' then + preprocessor_definitions += ' GDT_TEST=1' + end + s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY' => 'YES', - 'GCC_PREPROCESSOR_DEFINITIONS' => - # The nanopb pod sets these defs, so we must too. (We *do* require 16bit - # (or larger) fields, so we'd have to set at least PB_FIELD_16BIT - # anyways.) - 'PB_FIELD_32BIT=1 PB_NO_PACKED_STRUCTS=1 PB_ENABLE_MALLOC=1 GDTCOR_VERSION=' + s.version.to_s, + 'GCC_PREPROCESSOR_DEFINITIONS' => preprocessor_definitions }.merge(header_search_paths) common_test_sources = ['GoogleDataTransport/GDTCORTests/Common/**/*.{h,m}'] + common_cct_test_sources = [ + 'GoogleDataTransport/GDTCCTTests/Common/**/*.{h,m}', + 'GoogleDataTransport/GDTCCTTests/Unit/Helpers/**/*.{h,m}' + ] # Test app specs if ENV['GDT_DEV'] && ENV['GDT_DEV'] == '1' then @@ -92,7 +106,7 @@ Shared library for iOS SDK data transport needs. test_spec.scheme = { :code_coverage => true } test_spec.platforms = {:ios => ios_deployment_target, :osx => osx_deployment_target, :tvos => tvos_deployment_target} test_spec.requires_app_host = false - test_spec.source_files = ['GoogleDataTransport/GDTCORTests/Unit/**/*.{h,m}'] + common_test_sources + test_spec.source_files = ['GoogleDataTransport/GDTCORTests/Unit/**/*.{h,m}'] + common_test_sources + common_cct_test_sources test_spec.pod_target_xcconfig = header_search_paths end @@ -146,8 +160,6 @@ Shared library for iOS SDK data transport needs. end end - common_cct_test_sources = ['GoogleDataTransport/GDTCCTTests/Common/**/*.{h,m}'] - # Test specs s.test_spec 'CCT-Tests-Unit' do |test_spec| test_spec.scheme = { :code_coverage => true } diff --git a/GoogleDataTransport/GDTCCTLibrary/GDTCCTNanopbHelpers.m b/GoogleDataTransport/GDTCCTLibrary/GDTCCTNanopbHelpers.m index ece3baeb..84a93264 100644 --- a/GoogleDataTransport/GDTCCTLibrary/GDTCCTNanopbHelpers.m +++ b/GoogleDataTransport/GDTCCTLibrary/GDTCCTNanopbHelpers.m @@ -35,12 +35,12 @@ #pragma mark - General purpose encoders -pb_bytes_array_t *GDTCCTEncodeString(NSString *string) { +pb_bytes_array_t *_Nullable GDTCCTEncodeString(NSString *string) { NSData *stringBytes = [string dataUsingEncoding:NSUTF8StringEncoding]; return GDTCCTEncodeData(stringBytes); } -pb_bytes_array_t *GDTCCTEncodeData(NSData *data) { +pb_bytes_array_t *_Nullable GDTCCTEncodeData(NSData *data) { pb_bytes_array_t *pbBytesArray = calloc(1, PB_BYTES_ARRAY_T_ALLOCSIZE(data.length)); if (pbBytesArray != NULL) { [data getBytes:pbBytesArray->bytes length:data.length]; @@ -160,10 +160,33 @@ gdt_cct_LogEvent GDTCCTConstructLogEvent(GDTCOREvent *event) { return logEvent; } logEvent.source_extension = GDTCCTEncodeData(extensionBytes); // read bytes from the file. + if (event.productData) { + logEvent.compliance_data = GDTCCTConstructComplianceData(event.productData); + logEvent.has_compliance_data = 1; + } return logEvent; } -gdt_cct_ClientInfo GDTCCTConstructClientInfo() { +gdt_cct_ComplianceData GDTCCTConstructComplianceData(GDTCORProductData *productData) { + privacy_context_external_ExternalPRequestContext prequest = + privacy_context_external_ExternalPRequestContext_init_default; + prequest.origin_associated_product_id = productData.productID; + prequest.has_origin_associated_product_id = 1; + + privacy_context_external_ExternalPrivacyContext privacy_context = + privacy_context_external_ExternalPrivacyContext_init_default; + privacy_context.prequest = prequest; + privacy_context.has_prequest = 1; + + gdt_cct_ComplianceData complianceData = gdt_cct_ComplianceData_init_default; + complianceData.privacy_context = privacy_context; + complianceData.has_privacy_context = 1; + complianceData.product_id_origin = gdt_cct_ComplianceData_ProductIdOrigin_EVENT_OVERRIDE; + complianceData.has_product_id_origin = 1; + return complianceData; +} + +gdt_cct_ClientInfo GDTCCTConstructClientInfo(void) { gdt_cct_ClientInfo clientInfo = gdt_cct_ClientInfo_init_default; clientInfo.client_type = gdt_cct_ClientInfo_ClientType_IOS_FIREBASE; clientInfo.has_client_type = 1; @@ -171,12 +194,13 @@ gdt_cct_ClientInfo GDTCCTConstructClientInfo() { clientInfo.ios_client_info = GDTCCTConstructiOSClientInfo(); clientInfo.has_ios_client_info = 1; #elif TARGET_OS_OSX - // TODO(mikehaney24): Expand the proto to include macOS client info. + clientInfo.mac_client_info = GDTCCTConstructMacClientInfo(); + clientInfo.has_mac_client_info = 1; #endif return clientInfo; } -gdt_cct_IosClientInfo GDTCCTConstructiOSClientInfo() { +gdt_cct_IosClientInfo GDTCCTConstructiOSClientInfo(void) { gdt_cct_IosClientInfo iOSClientInfo = gdt_cct_IosClientInfo_init_default; #if TARGET_OS_IOS || TARGET_OS_TV UIDevice *device = [UIDevice currentDevice]; @@ -202,7 +226,35 @@ gdt_cct_IosClientInfo GDTCCTConstructiOSClientInfo() { return iOSClientInfo; } -NSData *GDTCCTConstructNetworkConnectionInfoData() { +gdt_cct_MacClientInfo GDTCCTConstructMacClientInfo(void) { + gdt_cct_MacClientInfo macOSClientInfo = gdt_cct_MacClientInfo_init_default; + + NSOperatingSystemVersion osVersion = [NSProcessInfo processInfo].operatingSystemVersion; + NSString *majorVersion = [@(osVersion.majorVersion) stringValue]; + NSString *minorVersion = [@(osVersion.minorVersion) stringValue]; + NSString *majorAndMinorString = [NSString stringWithFormat:@"%@.%@", majorVersion, minorVersion]; + macOSClientInfo.os_major_version = GDTCCTEncodeString(majorAndMinorString); + + NSString *patchVersion = [@(osVersion.patchVersion) stringValue]; + NSString *majorMinorPatchString = + [NSString stringWithFormat:@"%@.%@", majorAndMinorString, patchVersion]; + macOSClientInfo.os_full_version = GDTCCTEncodeString(majorMinorPatchString); + + NSBundle *bundle = [NSBundle mainBundle]; + NSString *version = [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; + if (version) { + macOSClientInfo.application_build = GDTCCTEncodeString(version); + } + + NSString *bundleID = bundle.bundleIdentifier; + if (bundleID) { + macOSClientInfo.application_bundle_id = GDTCCTEncodeString(bundleID); + } + + return macOSClientInfo; +} + +NSData *GDTCCTConstructNetworkConnectionInfoData(void) { gdt_cct_NetworkConnectionInfo networkConnectionInfo = gdt_cct_NetworkConnectionInfo_init_default; NSInteger currentNetworkType = GDTCORNetworkTypeMessage(); if (currentNetworkType) { @@ -223,7 +275,7 @@ gdt_cct_IosClientInfo GDTCCTConstructiOSClientInfo() { return networkConnectionInfoData; } -gdt_cct_NetworkConnectionInfo_MobileSubtype GDTCCTNetworkConnectionInfoNetworkMobileSubtype() { +gdt_cct_NetworkConnectionInfo_MobileSubtype GDTCCTNetworkConnectionInfoNetworkMobileSubtype(void) { NSNumber *networkMobileSubtypeMessage = @(GDTCORNetworkMobileSubTypeMessage()); if (!networkMobileSubtypeMessage.intValue) { return gdt_cct_NetworkConnectionInfo_MobileSubtype_UNKNOWN_MOBILE_SUBTYPE; diff --git a/GoogleDataTransport/GDTCCTLibrary/GDTCCTURLSessionDataResponse.m b/GoogleDataTransport/GDTCCTLibrary/GDTCCTURLSessionDataResponse.m new file mode 100644 index 00000000..669e93fe --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/GDTCCTURLSessionDataResponse.m @@ -0,0 +1,28 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTURLSessionDataResponse.h" + +@implementation GDTCCTURLSessionDataResponse + +- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(NSData *)body { + self = [super init]; + if (self) { + _HTTPResponse = response; + _HTTPBody = body; + } + return self; +} + +@end diff --git a/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploadOperation.m b/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploadOperation.m index fd9507e3..0b85840a 100644 --- a/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploadOperation.m +++ b/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploadOperation.m @@ -25,6 +25,7 @@ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" #import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadBatch.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" @@ -33,10 +34,10 @@ #import #import -#import -#import #import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTCompressionHelper.h" #import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h" +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTURLSessionDataResponse.h" +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCOREvent+GDTMetricsSupport.h" #import "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h" @@ -68,6 +69,16 @@ @interface GDTCCTUploadOperation () @property(nonatomic, readonly) NSURL *uploadURL; @property(nonatomic, readonly) id storage; @property(nonatomic, readonly) id metadataProvider; +@property(nonatomic, readonly, nullable) id metricsController; + +/** The URL session that will attempt upload. */ +@property(nonatomic, nullable) NSURLSession *uploaderSession; + +/// The metrics being uploaded by the operation. These metrics are fetched and included as an event +/// in the upload batch as part of the upload process. +/// +/// Metrics being uploaded are retained so they can be re-stored if upload is not successful. +@property(nonatomic, nullable) GDTCORMetrics *currentMetrics; /// NSOperation state properties implementation. @property(nonatomic, readwrite, getter=isExecuting) BOOL executing; @@ -84,23 +95,31 @@ - (instancetype)initWithTarget:(GDTCORTarget)target uploadURL:(NSURL *)uploadURL queue:(dispatch_queue_t)queue storage:(id)storage - metadataProvider:(id)metadataProvider { + metadataProvider:(id)metadataProvider + metricsController:(nullable id)metricsController { self = [super init]; if (self) { _uploaderQueue = queue; - NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; - _uploaderSession = [NSURLSession sessionWithConfiguration:config - delegate:self - delegateQueue:nil]; _target = target; _conditions = conditions; _uploadURL = uploadURL; _storage = storage; _metadataProvider = metadataProvider; + _metricsController = metricsController; } return self; } +- (NSURLSession *)uploaderSessionCreateIfNeeded { + if (_uploaderSession == nil) { + NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; + _uploaderSession = [NSURLSession sessionWithConfiguration:config + delegate:self + delegateQueue:nil]; + } + return _uploaderSession; +} + - (void)uploadTarget:(GDTCORTarget)target withConditions:(GDTCORUploadConditions)conditions { __block GDTCORBackgroundIdentifier backgroundTaskID = GDTCORBackgroundIdentifierInvalid; @@ -116,11 +135,16 @@ - (void)uploadTarget:(GDTCORTarget)target withConditions:(GDTCORUploadConditions beginBackgroundTaskWithName:@"GDTCCTUploader-upload" expirationHandler:^{ if (backgroundTaskID != GDTCORBackgroundIdentifierInvalid) { - // Cancel the upload and complete delivery. - [self.currentTask cancel]; + // Cancel the upload. The outgoing upload network request + // is not cancelled and may or may not complete delivery. + [self cancel]; // End the background task. backgroundTaskCompletion(); + } else { + GDTCORLog(GDTCORMCDDebugLog, GDTCORLoggingLevelWarnings, + @"Attempted to cancel invalid background task in " + "GDTCCTUploadOperation."); } }]; @@ -128,59 +152,67 @@ - (void)uploadTarget:(GDTCORTarget)target withConditions:(GDTCORUploadConditions // 1. Check if the conditions for the target are suitable. [self isReadyToUploadTarget:target conditions:conditions] + .validateOn(self.uploaderQueue, + ^BOOL(NSNull *__unused _) { + // 2. Stop the operation if it has been cancelled. + return !self.isCancelled; + }) .thenOn(self.uploaderQueue, - ^id(id result) { - // 2. Remove previously attempted batches + ^FBLPromise *(NSNull *result) { + // 3. Remove previously attempted batches. return [storage removeAllBatchesForTarget:target deleteEvents:NO]; }) .thenOn(self.uploaderQueue, - ^FBLPromise *(id result) { + ^FBLPromise *(NSNull *__unused _) { // There may be a big amount of events stored, so creating a batch may be an // expensive operation. - // 3. Do a lightweight check if there are any events for the target first to - // finish early if there are no. + // 4. Do a lightweight check if there are any events for the target first to + // finish early if there are none. return [storage hasEventsForTarget:target]; }) .validateOn(self.uploaderQueue, ^BOOL(NSNumber *hasEvents) { - // Stop operation if there are no events to upload. + // 5. Stop operation if there are no events to upload. return hasEvents.boolValue; }) .thenOn(self.uploaderQueue, - ^FBLPromise *(id result) { - if (self.isCancelled) { - return nil; - } - - // 4. Fetch events to upload. + ^FBLPromise *(NSNumber *__unused _) { + // 6. Fetch events to upload. GDTCORStorageEventSelector *eventSelector = [self eventSelectorTarget:target withConditions:conditions]; return [storage batchWithEventSelector:eventSelector batchExpiration:[NSDate dateWithTimeIntervalSinceNow:600]]; }) + .thenOn(self.uploaderQueue, + ^FBLPromise *(GDTCORUploadBatch *batch) { + // 7. Add metrics to the batch if the target has a + // corresponding metrics controller. + if (!self.metricsController) { + return [FBLPromise resolvedWith:batch]; + } + + return [self batchByAddingMetricsEventToBatch:batch forTarget:target]; + }) .validateOn(self.uploaderQueue, - ^BOOL(GDTCORUploadBatch *batch) { - // 5. Validate batch. - return batch.batchID != nil && batch.events.count > 0; + ^BOOL(GDTCORUploadBatch *__unused _) { + // 8. Stop the operation if it has been cancelled. + return !self.isCancelled; }) .thenOn(self.uploaderQueue, ^FBLPromise *(GDTCORUploadBatch *batch) { // A non-empty batch has been created, consider it as an upload attempt. self.uploadAttempted = YES; - // 6. Perform upload URL request. - return [self sendURLRequestWithBatch:batch target:target storage:storage]; - }) - .thenOn(self.uploaderQueue, - ^id(id result) { - // 7. Finish operation. - [self finishOperation]; - backgroundTaskCompletion(); - return nil; + // 9. Perform upload. + return [self uploadBatch:batch toTarget:target storage:storage]; }) - .catchOn(self.uploaderQueue, ^(NSError *error) { - // TODO: Maybe report the error to the client. + .catchOn(self.uploaderQueue, + ^(NSError *error){ + // TODO: Consider reporting the error to the client. + }) + .alwaysOn(self.uploaderQueue, ^{ + // 10. Finish operation. [self finishOperation]; backgroundTaskCompletion(); }); @@ -188,53 +220,74 @@ - (void)uploadTarget:(GDTCORTarget)target withConditions:(GDTCORUploadConditions #pragma mark - Upload implementation details -/** Sends URL request to upload the provided batch and handle the response. */ -- (FBLPromise *)sendURLRequestWithBatch:(GDTCORUploadBatch *)batch - target:(GDTCORTarget)target - storage:(id)storage { - NSNumber *batchID = batch.batchID; - +/** Uploads a given batch from storage to a target. */ +- (FBLPromise *)uploadBatch:(GDTCORUploadBatch *)batch + toTarget:(GDTCORTarget)target + storage:(id)storage { // 1. Send URL request. return [self sendURLRequestWithBatch:batch target:target] - .thenOn( - self.uploaderQueue, - ^FBLPromise *(GULURLSessionDataResponse *response) { - // 2. Parse response and update the next upload time if can. - [self updateNextUploadTimeWithResponse:response forTarget:target]; - - // 3. Cleanup batch. - - // Only retry if one of these codes is returned: - // 429 - Too many requests; - // 5xx - Server errors. - NSInteger statusCode = response.HTTPResponse.statusCode; - if (statusCode == 429 || (statusCode >= 500 && statusCode < 600)) { - // Move the events back to the main storage to be uploaded on the next attempt. - return [storage removeBatchWithID:batchID deleteEvents:NO]; - } else { - if (statusCode >= 200 && statusCode <= 300) { - GDTCORLogDebug(@"CCT: batch %@ delivered", batchID); - } else { - GDTCORLogDebug( - @"CCT: batch %@ was rejected by the server and will be deleted with all events", - batchID); - } - - // The events are either delivered or unrecoverable broken, so remove the batch with - // events. - return [storage removeBatchWithID:batch.batchID deleteEvents:YES]; - } - }) + .thenOn(self.uploaderQueue, + ^FBLPromise *(GDTCCTURLSessionDataResponse *response) { + // 2. Update the next upload time and process response. + [self updateNextUploadTimeWithResponse:response forTarget:target]; + + return [self processResponse:response forBatch:batch storage:storage]; + }) .recoverOn(self.uploaderQueue, ^id(NSError *error) { - // In the case of a network error move the events back to the main storage to be uploaded on - // the next attempt. - return [storage removeBatchWithID:batchID deleteEvents:NO]; + // If a network error occurred, move the events back to the main + // storage so they can attempt to be uploaded in the next attempt. + // Additionally, if metrics were added to the batch, place them back + // in storage. + if (self.currentMetrics) { + [self.metricsController offerMetrics:self.currentMetrics]; + } + return [storage removeBatchWithID:batch.batchID deleteEvents:NO]; }); } +/** Processes a URL session response for a given batch from storage. */ +- (FBLPromise *)processResponse:(GDTCCTURLSessionDataResponse *)response + forBatch:(GDTCORUploadBatch *)batch + storage:(id)storage { + // Cleanup batch based on the response's status code. + NSInteger statusCode = response.HTTPResponse.statusCode; + BOOL isSuccess = statusCode >= 200 && statusCode < 300; + // Transient errors include "too many requests" (429) and server errors (5xx). + BOOL isTransientError = + statusCode == 429 || statusCode == 404 || (statusCode >= 500 && statusCode < 600); + + BOOL shouldDeleteEvents = isSuccess || !isTransientError; + + // If the batch included metrics and the upload failed, place metrics back + // in storage. + GDTCORMetrics *uploadedMetrics = [self currentMetrics]; + if (uploadedMetrics && !isSuccess) { + [self.metricsController offerMetrics:uploadedMetrics]; + } + + if (isSuccess) { + GDTCORLogDebug(@"CCT: batch %@ uploaded. Batch will be deleted.", batch.batchID); + + } else if (isTransientError) { + GDTCORLogDebug(@"CCT: batch %@ upload failed. Batch will attempt to be uploaded later.", + batch.batchID); + + } else { + GDTCORLogDebug(@"CCT: batch %@ upload failed. Batch will be deleted.", batch.batchID); + + if (/* isInvalidPayloadError */ statusCode == 400) { + // Log events that will be dropped due to the upload error. + [self.metricsController logEventsDroppedForReason:GDTCOREventDropReasonInvalidPayload + events:batch.events]; + } + } + + return [storage removeBatchWithID:batch.batchID deleteEvents:shouldDeleteEvents]; +} + /** Composes and sends URL request. */ -- (FBLPromise *)sendURLRequestWithBatch:(GDTCORUploadBatch *)batch - target:(GDTCORTarget)target { +- (FBLPromise *)sendURLRequestWithBatch:(GDTCORUploadBatch *)batch + target:(GDTCORTarget)target { return [FBLPromise onQueue:self.uploaderQueue do:^NSURLRequest * { @@ -254,14 +307,43 @@ - (void)uploadTarget:(GDTCORTarget)target withConditions:(GDTCORUploadConditions return request; }] .thenOn(self.uploaderQueue, - ^FBLPromise *(NSURLRequest *request) { + ^FBLPromise *(NSURLRequest *request) { // 2. Send URL request. - return [self.uploaderSession gul_dataTaskPromiseWithRequest:request]; - }); + NSURLSession *session = [self uploaderSessionCreateIfNeeded]; + return [FBLPromise wrapObjectOrErrorCompletion:^( + FBLPromiseObjectOrErrorCompletion _Nonnull handler) { + [[session dataTaskWithRequest:request + completionHandler:^(NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + handler(nil, error); + } else { + handler([[GDTCCTURLSessionDataResponse alloc] + initWithResponse:(NSHTTPURLResponse *)response + HTTPBody:data], + nil); + } + }] resume]; + }]; + }) + .thenOn(self.uploaderQueue, + ^GDTCCTURLSessionDataResponse *(GDTCCTURLSessionDataResponse *response) { + // Invalidate session to release the delegate (which is `self`) to break the retain + // cycle. + [self.uploaderSession finishTasksAndInvalidate]; + return response; + }) + .recoverOn(self.uploaderQueue, ^id(NSError *error) { + // Invalidate session to release the delegate (which is `self`) to break the retain cycle. + [self.uploaderSession finishTasksAndInvalidate]; + // Re-throw the error. + return error; + }); } /** Parses server response and update next upload time for the specified target based on it. */ -- (void)updateNextUploadTimeWithResponse:(GULURLSessionDataResponse *)response +- (void)updateNextUploadTimeWithResponse:(GDTCCTURLSessionDataResponse *)response forTarget:(GDTCORTarget)target { GDTCORClock *futureUploadTime; if (response.HTTPBody) { @@ -463,6 +545,27 @@ - (GDTCORStorageEventSelector *)eventSelectorTarget:(GDTCORTarget)target qosTiers:qosTiers]; } +- (FBLPromise *)batchByAddingMetricsEventToBatch:(GDTCORUploadBatch *)batch + forTarget:(GDTCORTarget)target { + return [self.metricsController getAndResetMetrics] + .thenOn(self.uploaderQueue, + ^GDTCORUploadBatch *(GDTCORMetrics *metrics) { + // Save the metrics so they can be re-stored if upload fails. + [self setCurrentMetrics:metrics]; + + GDTCOREvent *metricsEvent = [GDTCOREvent eventWithMetrics:metrics forTarget:target]; + GDTCORUploadBatch *batchWithMetricEvent = [[GDTCORUploadBatch alloc] + initWithBatchID:batch.batchID + events:[batch.events setByAddingObject:metricsEvent]]; + + return batchWithMetricEvent; + }) + .recoverOn(self.uploaderQueue, ^GDTCORUploadBatch *(NSError *error) { + // Return given batch if an error occurs (i.e. no metrics were fetched). + return batch; + }); +} + #pragma mark - NSURLSessionDelegate - (void)URLSession:(NSURLSession *)session @@ -488,29 +591,45 @@ - (void)URLSession:(NSURLSession *)session @synthesize executing = _executing; @synthesize finished = _finished; +- (BOOL)isFinished { + @synchronized(self) { + return _finished; + } +} + +- (BOOL)isExecuting { + @synchronized(self) { + return _executing; + } +} + - (BOOL)isAsynchronous { return YES; } - (void)startOperation { - [self willChangeValueForKey:@"isExecuting"]; - [self willChangeValueForKey:@"isFinished"]; - _executing = YES; - _finished = NO; - [self didChangeValueForKey:@"isExecuting"]; - [self didChangeValueForKey:@"isFinished"]; + @synchronized(self) { + [self willChangeValueForKey:@"isExecuting"]; + [self willChangeValueForKey:@"isFinished"]; + self->_executing = YES; + self->_finished = NO; + [self didChangeValueForKey:@"isExecuting"]; + [self didChangeValueForKey:@"isFinished"]; + } } - (void)finishOperation { - [self willChangeValueForKey:@"isExecuting"]; - [self willChangeValueForKey:@"isFinished"]; - _executing = NO; - _finished = YES; - [self didChangeValueForKey:@"isExecuting"]; - [self didChangeValueForKey:@"isFinished"]; + @synchronized(self) { + [self willChangeValueForKey:@"isExecuting"]; + [self willChangeValueForKey:@"isFinished"]; + self->_executing = NO; + self->_finished = YES; + [self didChangeValueForKey:@"isExecuting"]; + [self didChangeValueForKey:@"isFinished"]; + } } -- (void)main { +- (void)start { [self startOperation]; GDTCORLogDebug(@"Upload operation started: %@", self); @@ -518,16 +637,33 @@ - (void)main { } - (void)cancel { - GDTCORLogDebug(@"Upload operation cancelled: %@", self); - [super cancel]; + @synchronized(self) { + [super cancel]; - // If the operation hasn't been started we can set `isFinished = YES` straight away. - if (!_executing) { - _executing = NO; - _finished = YES; + // If the operation hasn't been started we can set `isFinished = YES` straight away. + if (!_executing) { + _executing = NO; + _finished = YES; + } } } +#pragma mark - Force Category Linking + +extern void GDTCCTInclude_GDTCOREvent_GDTCCTSupport_Category(void); +extern void GDTCCTInclude_GDTCOREvent_GDTMetricsSupport_Category(void); +extern void GDTCCTInclude_GDTCORLogSourceMetrics_Internal_Category(void); + +/// Does nothing when called, and not meant to be called. +/// +/// This method forces the linker to include categories even if +/// users do not include the '-ObjC' linker flag in their project. ++ (void)noop { + GDTCCTInclude_GDTCOREvent_GDTCCTSupport_Category(); + GDTCCTInclude_GDTCOREvent_GDTMetricsSupport_Category(); + GDTCCTInclude_GDTCORLogSourceMetrics_Internal_Category(); +} + @end NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploader.m b/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploader.m index d25dc542..c436ed31 100644 --- a/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploader.m +++ b/GoogleDataTransport/GDTCCTLibrary/GDTCCTUploader.m @@ -42,6 +42,9 @@ @implementation GDTCCTUploader + (void)load { GDTCCTUploader *uploader = [GDTCCTUploader sharedInstance]; +#if GDT_TEST + [[GDTCORRegistrar sharedInstance] registerUploader:uploader target:kGDTCORTargetTest]; +#endif // GDT_TEST [[GDTCORRegistrar sharedInstance] registerUploader:uploader target:kGDTCORTargetCCT]; [[GDTCORRegistrar sharedInstance] registerUploader:uploader target:kGDTCORTargetFLL]; [[GDTCORRegistrar sharedInstance] registerUploader:uploader target:kGDTCORTargetCSH]; @@ -89,13 +92,17 @@ - (void)uploadTarget:(GDTCORTarget)target withConditions:(GDTCORUploadConditions return; } + id metricsController = + GDTCORMetricsControllerInstanceForTarget(target); + GDTCCTUploadOperation *uploadOperation = [[GDTCCTUploadOperation alloc] initWithTarget:target conditions:conditions uploadURL:[[self class] serverURLForTarget:target] queue:self.uploadQueue storage:storage - metadataProvider:self]; + metadataProvider:self + metricsController:metricsController]; GDTCORLogDebug(@"Upload operation created: %@, target: %@", uploadOperation, @(target)); @@ -134,11 +141,11 @@ + (NSURL *_Nullable)testServerURL { } + (nullable NSURL *)serverURLForTarget:(GDTCORTarget)target { -#if !NDEBUG +#if GDT_TEST if (_testServerURL) { return _testServerURL; } -#endif // !NDEBUG +#endif // GDT_TEST return [GDTCOREndpoints uploadURLForTarget:target]; } @@ -186,8 +193,7 @@ - (nullable NSString *)APIKeyForTarget:(GDTCORTarget)target { return nil; } -#if !NDEBUG - +#if GDT_TEST - (BOOL)waitForUploadFinishedWithTimeout:(NSTimeInterval)timeout { NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:timeout]; while ([expirationDate compare:[NSDate date]] == NSOrderedDescending) { @@ -202,8 +208,7 @@ - (BOOL)waitForUploadFinishedWithTimeout:(NSTimeInterval)timeout { self.uploadOperationQueue.operations); return NO; } - -#endif // !NDEBUG +#endif // GDT_TEST @end diff --git a/GoogleDataTransport/GDTCCTLibrary/GDTCOREvent+GDTCCTSupport.m b/GoogleDataTransport/GDTCCTLibrary/GDTCOREvent+GDTCCTSupport.m index 00a0f1b4..f102c81e 100644 --- a/GoogleDataTransport/GDTCCTLibrary/GDTCOREvent+GDTCCTSupport.m +++ b/GoogleDataTransport/GDTCCTLibrary/GDTCOREvent+GDTCCTSupport.m @@ -130,6 +130,10 @@ - (nullable NSData *)networkConnectionInfoData { options:0 error:&error]; NSString *base64Data = bytesDict[GDTCCTNetworkConnectionInfo]; + if (base64Data == nil) { + return nil; + } + NSData *networkConnectionInfoData = [[NSData alloc] initWithBase64EncodedString:base64Data options:0]; if (error) { @@ -234,3 +238,7 @@ - (void)setEventCode:(NSNumber *)eventCode { } @end + +/// Stub used to force the linker to include the categories in this file. +void GDTCCTInclude_GDTCOREvent_GDTCCTSupport_Category(void) { +} diff --git a/GoogleDataTransport/GDTCCTLibrary/GDTCOREvent+GDTMetricsSupport.m b/GoogleDataTransport/GDTCCTLibrary/GDTCOREvent+GDTMetricsSupport.m new file mode 100644 index 00000000..6386835c --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/GDTCOREvent+GDTMetricsSupport.m @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCOREvent+GDTMetricsSupport.h" + +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCORMetrics+GDTCCTSupport.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" + +/// The mapping ID that represents the `LogSource` for GDT metrics. +static NSString *const kMetricEventMappingID = @"1710"; + +@implementation GDTCOREvent (GDTMetricsSupport) + ++ (GDTCOREvent *)eventWithMetrics:(GDTCORMetrics *)metrics forTarget:(GDTCORTarget)target { + GDTCOREvent *metricsEvent = [[GDTCOREvent alloc] initWithMappingID:kMetricEventMappingID + target:target]; + metricsEvent.dataObject = metrics; + + return metricsEvent; +} + +@end + +/// Stub used to force the linker to include the categories in this file. +void GDTCCTInclude_GDTCOREvent_GDTMetricsSupport_Category(void) { +} diff --git a/GoogleDataTransport/GDTCCTLibrary/GDTCORMetrics+GDTCCTSupport.m b/GoogleDataTransport/GDTCCTLibrary/GDTCORMetrics+GDTCCTSupport.m new file mode 100644 index 00000000..044b85a1 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/GDTCORMetrics+GDTCCTSupport.m @@ -0,0 +1,211 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCORMetrics+GDTCCTSupport.h" + +#import +#import +#import + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h" + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h" +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageSizeBytes.h" + +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" + +#import "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.h" + +typedef NSDictionary GDTCORDroppedEventCounter; + +@interface GDTCORLogSourceMetrics (Internal) + +/// A dictionary of log sources that map to counters that reflect the number of events dropped for a +/// given set of reasons (``GDTCOREventDropReason``). +@property(nonatomic, readonly) + NSDictionary *droppedEventCounterByLogSource; + +@end + +@implementation GDTCORMetrics (GDTCCTSupport) + +- (NSData *)transportBytes { + // Create and populate proto. + gdt_client_metrics_ClientMetrics clientMetricsProto = + gdt_client_metrics_ClientMetrics_init_default; + + clientMetricsProto.window = + GDTCCTConstructTimeWindow(self.collectionStartDate, self.collectionEndDate); + + clientMetricsProto.log_source_metrics = GDTCCTConstructLogSourceMetrics(self.logSourceMetrics); + clientMetricsProto.log_source_metrics_count = + GDTCCTGetLogSourceMetricsCount(self.logSourceMetrics); + + clientMetricsProto.global_metrics = + GDTCCTConstructGlobalMetrics(self.currentCacheSize, self.maxCacheSize); + + clientMetricsProto.app_namespace = GDTCCTEncodeString(self.bundleID); + + // Encode proto into a data buffer. + pb_ostream_t sizeStream = PB_OSTREAM_SIZING; + + // - Encode 1 time to determine the expected size of the buffer. + if (!pb_encode(&sizeStream, gdt_client_metrics_ClientMetrics_fields, &clientMetricsProto)) { + GDTCORLogError(GDTCORMCETransportBytesError, @"Error in nanopb encoding for size: %s", + PB_GET_ERROR(&sizeStream)); + } + + // - Encode a 2nd time to actually copy the proto's bytes into the buffer. + size_t bufferSize = sizeStream.bytes_written; + CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize); + CFDataSetLength(dataRef, bufferSize); + pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize); + if (!pb_encode(&ostream, gdt_client_metrics_ClientMetrics_fields, &clientMetricsProto)) { + GDTCORLogError(GDTCORMCETransportBytesError, @"Error in nanopb encoding for size: %s", + PB_GET_ERROR(&ostream)); + } + CFDataSetLength(dataRef, ostream.bytes_written); + + // Release the allocated proto. + pb_release(gdt_client_metrics_ClientMetrics_fields, &clientMetricsProto); + + return CFBridgingRelease(dataRef); +} + +/// Constructs and returns a ``gdt_client_metrics_LogSourceMetrics`` from the given log source +/// metrics. +/// @param logSourceMetrics The given log source metrics. +gdt_client_metrics_LogSourceMetrics *GDTCCTConstructLogSourceMetrics( + GDTCORLogSourceMetrics *logSourceMetrics) { + // The metrics proto is a repeating field where each element represents the + // dropped event data for a log source (mapping ID). + NSUInteger logMetricsCount = logSourceMetrics.droppedEventCounterByLogSource.count; + gdt_client_metrics_LogSourceMetrics *repeatedLogSourceMetrics = + calloc(logMetricsCount, sizeof(gdt_client_metrics_LogSourceMetrics)); + + // Each log source (mapping ID) has a corresponding dropped event counter. + // Enumerate over the dictionary of log source and, for each log source, + // (mapping ID) create a proto representation of the number of events dropped + // for each given reason. + __block NSUInteger logSourceIndex = 0; + [logSourceMetrics.droppedEventCounterByLogSource + enumerateKeysAndObjectsUsingBlock:^(NSString *logSource, + GDTCORDroppedEventCounter *eventCounterForLogSource, + BOOL *__unused _) { + // Create the log source proto for the given mapping ID. It contains a + // repeating field to encapsulate the number of events dropped for each + // given drop reason. + __block gdt_client_metrics_LogSourceMetrics logSourceMetrics = + gdt_client_metrics_LogSourceMetrics_init_zero; + logSourceMetrics.log_source = GDTCCTEncodeString(logSource); + logSourceMetrics.log_event_dropped_count = (pb_size_t)eventCounterForLogSource.count; + logSourceMetrics.log_event_dropped = + calloc(eventCounterForLogSource.count, sizeof(gdt_client_metrics_LogEventDropped)); + + // Each dropped event counter counts the number of events dropped for + // each drop reason. Enumerate over all of these counters to populate + // the log source proto's repeating field of event drop data. + __block NSUInteger eventCounterIndex = 0; + [eventCounterForLogSource + enumerateKeysAndObjectsUsingBlock:^(NSNumber *eventDropReason, + NSNumber *droppedEventCount, BOOL *__unused _) { + gdt_client_metrics_LogEventDropped droppedEvents = + gdt_client_metrics_LogEventDropped_init_zero; + droppedEvents.events_dropped_count = droppedEventCount.integerValue; + droppedEvents.reason = + GDTCCTConvertEventDropReasonToProtoReason(eventDropReason.integerValue); + + // Append the dropped events proto to the repeated field and + // increment the index used for appending. + logSourceMetrics.log_event_dropped[eventCounterIndex] = droppedEvents; + eventCounterIndex += 1; + }]; + + // Append the metrics for the given log source (mappingID) to the + // repeated field and increment the index used for appending. + repeatedLogSourceMetrics[logSourceIndex] = logSourceMetrics; + logSourceIndex += 1; + }]; + + return repeatedLogSourceMetrics; +} + +/// Returns the count of log sources that have event drop metrics. +/// @param logSourceMetrics The given log source metrics. +pb_size_t GDTCCTGetLogSourceMetricsCount(GDTCORLogSourceMetrics *logSourceMetrics) { + return (pb_size_t)logSourceMetrics.droppedEventCounterByLogSource.count; +} + +/// Constructs and returns a ``gdt_client_metrics_TimeWindow`` proto from the given parameters. +/// @param collectionStartDate The start of the time window. +/// @param collectionEndDate The end of the time window. +gdt_client_metrics_TimeWindow GDTCCTConstructTimeWindow(NSDate *collectionStartDate, + NSDate *collectionEndDate) { + gdt_client_metrics_TimeWindow timeWindow = gdt_client_metrics_TimeWindow_init_zero; + // `- [NSDate timeIntervalSince1970]` returns a time interval in seconds so + // multiply by 1000 to convert to milliseconds. + timeWindow.start_ms = (int64_t)collectionStartDate.timeIntervalSince1970 * 1000; + timeWindow.end_ms = (int64_t)collectionEndDate.timeIntervalSince1970 * 1000; + return timeWindow; +} + +/// Constructs and returns a ``gdt_client_metrics_GlobalMetrics`` proto from the given parameters. +/// @param currentCacheSize The current cache size. +/// @param maxCacheSize The max cache size. +gdt_client_metrics_GlobalMetrics GDTCCTConstructGlobalMetrics(uint64_t currentCacheSize, + uint64_t maxCacheSize) { + gdt_client_metrics_StorageMetrics storageMetrics = gdt_client_metrics_StorageMetrics_init_zero; + storageMetrics.current_cache_size_bytes = currentCacheSize; + storageMetrics.max_cache_size_bytes = maxCacheSize; + + gdt_client_metrics_GlobalMetrics globalMetrics = gdt_client_metrics_GlobalMetrics_init_zero; + globalMetrics.storage_metrics = storageMetrics; + + return globalMetrics; +} + +/// Returns the corresponding ``gdt_client_metrics_LogEventDropped_Reason`` for the given +/// ``GDTCOREventDropReason``. +/// +/// To represent ``GDTCOREventDropReason`` in a proto, the reason must be mapped to a +/// ``gdt_client_metrics_LogEventDropped_Reason``. +/// +/// @param reason The ``GDTCOREventDropReason`` to represent in a proto. +gdt_client_metrics_LogEventDropped_Reason GDTCCTConvertEventDropReasonToProtoReason( + GDTCOREventDropReason reason) { + switch (reason) { + case GDTCOREventDropReasonUnknown: + return gdt_client_metrics_LogEventDropped_Reason_REASON_UNKNOWN; + case GDTCOREventDropReasonMessageTooOld: + return gdt_client_metrics_LogEventDropped_Reason_MESSAGE_TOO_OLD; + case GDTCOREventDropReasonStorageFull: + return gdt_client_metrics_LogEventDropped_Reason_CACHE_FULL; + case GDTCOREventDropReasonPayloadTooBig: + return gdt_client_metrics_LogEventDropped_Reason_PAYLOAD_TOO_BIG; + case GDTCOREventDropReasonMaxRetriesReached: + return gdt_client_metrics_LogEventDropped_Reason_MAX_RETRIES_REACHED; + case GDTCOREventDropReasonInvalidPayload: + // The below typo (`PAYLOD`) is currently checked in to g3. + return gdt_client_metrics_LogEventDropped_Reason_INVALID_PAYLOD; + case GDTCOREventDropReasonServerError: + return gdt_client_metrics_LogEventDropped_Reason_SERVER_ERROR; + } +} + +@end + +/// Stub used to force the linker to include the categories in this file. +void GDTCCTInclude_GDTCORLogSourceMetrics_Internal_Category(void) { +} diff --git a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h index 8372cf90..a0cd81f3 100644 --- a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h +++ b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h @@ -18,8 +18,10 @@ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORReachability.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h" #import "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h" +#import "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.h" NS_ASSUME_NONNULL_BEGIN @@ -32,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN * @param string The string to convert. * @return A newly allocated array of bytes representing the UTF8 encoding of the string. */ -pb_bytes_array_t *GDTCCTEncodeString(NSString *string); +pb_bytes_array_t *_Nullable GDTCCTEncodeString(NSString *string); /** Converts an NSData to a pb_bytes_array_t*. * @@ -41,7 +43,7 @@ pb_bytes_array_t *GDTCCTEncodeString(NSString *string); * @param data The data to convert. * @return A newly allocated array of bytes with [data bytes] copied into it. */ -pb_bytes_array_t *GDTCCTEncodeData(NSData *data); +pb_bytes_array_t *_Nullable GDTCCTEncodeData(NSData *data); #pragma mark - CCT object constructors @@ -83,6 +85,13 @@ gdt_cct_LogRequest GDTCCTConstructLogRequest(int32_t logSource, NSSet + +NS_ASSUME_NONNULL_BEGIN + +/** The class represents HTTP response received from `NSURLSession`. */ +@interface GDTCCTURLSessionDataResponse : NSObject + +@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; +@property(nonatomic, nullable, readonly) NSData *HTTPBody; + +- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploadOperation.h b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploadOperation.h index be4ab399..de234d5a 100644 --- a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploadOperation.h +++ b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploadOperation.h @@ -19,6 +19,7 @@ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORUploader.h" @protocol GDTCORStoragePromiseProtocol; +@protocol GDTCORMetricsControllerProtocol; NS_ASSUME_NONNULL_BEGIN @@ -42,22 +43,24 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -/** The designated initializer. - * @param target The events target to upload. - * @param conditions A set of upload conditions. The conditions affect the set of events to be - * uploaded, e.g. events with some QoS are not uploaded on a cellular network, etc. - * @param uploadURL The backend URL to upload the events. - * @param queue A queue to dispatch async upload steps. - * @param storage A storage object to fetch events for upload. - * @param metadataProvider An object to retrieve/update data shared between different upload - * operations. - */ +/// Designated initializer. +/// @param target The events target to upload. +/// @param conditions A set of upload conditions. The conditions affect the set of events to be +/// uploaded, e.g. events with some QoS are not uploaded on a cellular network, etc. +/// @param uploadURL The backend URL to upload the events. +/// @param queue A queue to dispatch async upload steps. +/// @param storage A storage object to fetch events for upload. +/// @param metadataProvider An object to retrieve/update data shared between upload operations. +/// @param metricsController The metrics controller corresponding to the given target. If the given +/// target does not support metrics controller, `nil` should be passed. +/// @return An individual operation that can be added to an operation queue. - (instancetype)initWithTarget:(GDTCORTarget)target conditions:(GDTCORUploadConditions)conditions uploadURL:(NSURL *)uploadURL queue:(dispatch_queue_t)queue storage:(id)storage metadataProvider:(id)metadataProvider + metricsController:(nullable id)metricsController NS_DESIGNATED_INITIALIZER; /** YES if a batch upload attempt was performed. NO otherwise. If NO for the finished operation, @@ -67,12 +70,6 @@ NS_ASSUME_NONNULL_BEGIN /** The queue on which all CCT uploading will occur. */ @property(nonatomic, readonly) dispatch_queue_t uploaderQueue; -/** The URL session that will attempt upload. */ -@property(nonatomic, readonly) NSURLSession *uploaderSession; - -/** The current upload task. */ -@property(nullable, nonatomic, readonly) NSURLSessionUploadTask *currentTask; - @end NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploader.h b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploader.h index 876fbe10..36934c68 100644 --- a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploader.h +++ b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploader.h @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)sharedInstance; -#if !NDEBUG +#if GDT_TEST /** An upload URL used across all targets. For testing only. */ @property(class, nullable, nonatomic) NSURL *testServerURL; @@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)waitForUploadFinishedWithTimeout:(NSTimeInterval)timeout; -#endif // !NDEBUG +#endif // GDT_TEST @end diff --git a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCOREvent+GDTMetricsSupport.h b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCOREvent+GDTMetricsSupport.h new file mode 100644 index 00000000..84d36ae3 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCOREvent+GDTMetricsSupport.h @@ -0,0 +1,34 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h" + +@class GDTCORMetrics; + +NS_ASSUME_NONNULL_BEGIN + +@interface GDTCOREvent (GDTMetricsSupport) + +/// Creates and returns an event for the given target with the given metrics. +/// @param metrics The metrics to set at the event's data. +/// @param target The backend target that the event corresponds to. ++ (GDTCOREvent *)eventWithMetrics:(GDTCORMetrics *)metrics forTarget:(GDTCORTarget)target; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTLibrary/Private/GDTCORMetrics+GDTCCTSupport.h b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCORMetrics+GDTCCTSupport.h new file mode 100644 index 00000000..8d1e6968 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Private/GDTCORMetrics+GDTCCTSupport.h @@ -0,0 +1,25 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventDataObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GDTCORMetrics (GDTCCTSupport) + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.c b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.c index 2f5327e5..8e5412e6 100644 --- a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.c +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.c @@ -15,7 +15,7 @@ */ /* Automatically generated nanopb constant definitions */ -/* Generated by nanopb-0.3.9.7 */ +/* Generated by nanopb-0.3.9.9 */ #include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h" @@ -30,13 +30,14 @@ const gdt_cct_QosTierConfiguration_QosTier gdt_cct_LogRequest_qos_tier_default = const int32_t gdt_cct_QosTierConfiguration_log_source_default = 0; -const pb_field_t gdt_cct_LogEvent_fields[7] = { +const pb_field_t gdt_cct_LogEvent_fields[8] = { PB_FIELD( 1, INT64 , OPTIONAL, STATIC , FIRST, gdt_cct_LogEvent, event_time_ms, event_time_ms, 0), PB_FIELD( 6, BYTES , OPTIONAL, POINTER , OTHER, gdt_cct_LogEvent, source_extension, event_time_ms, 0), PB_FIELD( 11, INT32 , OPTIONAL, STATIC , OTHER, gdt_cct_LogEvent, event_code, source_extension, 0), PB_FIELD( 15, SINT64 , OPTIONAL, STATIC , OTHER, gdt_cct_LogEvent, timezone_offset_seconds, event_code, 0), PB_FIELD( 17, INT64 , OPTIONAL, STATIC , OTHER, gdt_cct_LogEvent, event_uptime_ms, timezone_offset_seconds, 0), PB_FIELD( 23, MESSAGE , OPTIONAL, STATIC , OTHER, gdt_cct_LogEvent, network_connection_info, event_uptime_ms, &gdt_cct_NetworkConnectionInfo_fields), + PB_FIELD( 33, MESSAGE , OPTIONAL, STATIC , OTHER, gdt_cct_LogEvent, compliance_data, network_connection_info, &gdt_cct_ComplianceData_fields), PB_LAST_FIELD }; @@ -46,6 +47,14 @@ const pb_field_t gdt_cct_NetworkConnectionInfo_fields[3] = { PB_LAST_FIELD }; +const pb_field_t gdt_cct_MacClientInfo_fields[5] = { + PB_FIELD( 1, BYTES , OPTIONAL, POINTER , FIRST, gdt_cct_MacClientInfo, os_major_version, os_major_version, 0), + PB_FIELD( 2, BYTES , OPTIONAL, POINTER , OTHER, gdt_cct_MacClientInfo, os_full_version, os_major_version, 0), + PB_FIELD( 3, BYTES , OPTIONAL, POINTER , OTHER, gdt_cct_MacClientInfo, application_build, os_full_version, 0), + PB_FIELD( 7, BYTES , OPTIONAL, POINTER , OTHER, gdt_cct_MacClientInfo, application_bundle_id, application_build, 0), + PB_LAST_FIELD +}; + const pb_field_t gdt_cct_IosClientInfo_fields[8] = { PB_FIELD( 3, BYTES , OPTIONAL, POINTER , FIRST, gdt_cct_IosClientInfo, os_major_version, os_major_version, 0), PB_FIELD( 4, BYTES , OPTIONAL, POINTER , OTHER, gdt_cct_IosClientInfo, os_full_version, os_major_version, 0), @@ -57,9 +66,10 @@ const pb_field_t gdt_cct_IosClientInfo_fields[8] = { PB_LAST_FIELD }; -const pb_field_t gdt_cct_ClientInfo_fields[3] = { +const pb_field_t gdt_cct_ClientInfo_fields[4] = { PB_FIELD( 1, UENUM , OPTIONAL, STATIC , FIRST, gdt_cct_ClientInfo, client_type, client_type, 0), PB_FIELD( 4, MESSAGE , OPTIONAL, STATIC , OTHER, gdt_cct_ClientInfo, ios_client_info, client_type, &gdt_cct_IosClientInfo_fields), + PB_FIELD( 13, MESSAGE , OPTIONAL, STATIC , OTHER, gdt_cct_ClientInfo, mac_client_info, ios_client_info, &gdt_cct_MacClientInfo_fields), PB_LAST_FIELD }; @@ -105,23 +115,23 @@ const pb_field_t gdt_cct_LogResponse_fields[3] = { #if !defined(PB_FIELD_32BIT) /* If you get an error here, it means that you need to define PB_FIELD_32BIT * compile-time option. You can do that in pb.h or on compiler command line. - * + * * The reason you need to do this is that some of your messages contain tag * numbers or field sizes that are larger than what can fit in 8 or 16 bit * field descriptors. */ -PB_STATIC_ASSERT((pb_membersize(gdt_cct_LogEvent, network_connection_info) < 65536 && pb_membersize(gdt_cct_ClientInfo, ios_client_info) < 65536 && pb_membersize(gdt_cct_LogRequest, client_info) < 65536 && pb_membersize(gdt_cct_LogResponse, qos_tier) < 65536), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_gdt_cct_LogEvent_gdt_cct_NetworkConnectionInfo_gdt_cct_IosClientInfo_gdt_cct_ClientInfo_gdt_cct_BatchedLogRequest_gdt_cct_LogRequest_gdt_cct_QosTierConfiguration_gdt_cct_QosTiersOverride_gdt_cct_LogResponse) +PB_STATIC_ASSERT((pb_membersize(gdt_cct_LogEvent, network_connection_info) < 65536 && pb_membersize(gdt_cct_LogEvent, compliance_data) < 65536 && pb_membersize(gdt_cct_ClientInfo, ios_client_info) < 65536 && pb_membersize(gdt_cct_ClientInfo, mac_client_info) < 65536 && pb_membersize(gdt_cct_LogRequest, client_info) < 65536 && pb_membersize(gdt_cct_LogResponse, qos_tier) < 65536), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_gdt_cct_LogEvent_gdt_cct_NetworkConnectionInfo_gdt_cct_MacClientInfo_gdt_cct_IosClientInfo_gdt_cct_ClientInfo_gdt_cct_BatchedLogRequest_gdt_cct_LogRequest_gdt_cct_QosTierConfiguration_gdt_cct_QosTiersOverride_gdt_cct_LogResponse) #endif #if !defined(PB_FIELD_16BIT) && !defined(PB_FIELD_32BIT) /* If you get an error here, it means that you need to define PB_FIELD_16BIT * compile-time option. You can do that in pb.h or on compiler command line. - * + * * The reason you need to do this is that some of your messages contain tag * numbers or field sizes that are larger than what can fit in the default * 8 bit descriptors. */ -PB_STATIC_ASSERT((pb_membersize(gdt_cct_LogEvent, network_connection_info) < 256 && pb_membersize(gdt_cct_ClientInfo, ios_client_info) < 256 && pb_membersize(gdt_cct_LogRequest, client_info) < 256 && pb_membersize(gdt_cct_LogResponse, qos_tier) < 256), YOU_MUST_DEFINE_PB_FIELD_16BIT_FOR_MESSAGES_gdt_cct_LogEvent_gdt_cct_NetworkConnectionInfo_gdt_cct_IosClientInfo_gdt_cct_ClientInfo_gdt_cct_BatchedLogRequest_gdt_cct_LogRequest_gdt_cct_QosTierConfiguration_gdt_cct_QosTiersOverride_gdt_cct_LogResponse) +PB_STATIC_ASSERT((pb_membersize(gdt_cct_LogEvent, network_connection_info) < 256 && pb_membersize(gdt_cct_LogEvent, compliance_data) < 256 && pb_membersize(gdt_cct_ClientInfo, ios_client_info) < 256 && pb_membersize(gdt_cct_ClientInfo, mac_client_info) < 256 && pb_membersize(gdt_cct_LogRequest, client_info) < 256 && pb_membersize(gdt_cct_LogResponse, qos_tier) < 256), YOU_MUST_DEFINE_PB_FIELD_16BIT_FOR_MESSAGES_gdt_cct_LogEvent_gdt_cct_NetworkConnectionInfo_gdt_cct_MacClientInfo_gdt_cct_IosClientInfo_gdt_cct_ClientInfo_gdt_cct_BatchedLogRequest_gdt_cct_LogRequest_gdt_cct_QosTierConfiguration_gdt_cct_QosTiersOverride_gdt_cct_LogResponse) #endif diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h index 05bdf589..a13a16cd 100644 --- a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h @@ -15,12 +15,14 @@ */ /* Automatically generated nanopb header */ -/* Generated by nanopb-0.3.9.7 */ +/* Generated by nanopb-0.3.9.9 */ #ifndef PB_GDT_CCT_CCT_NANOPB_H_INCLUDED #define PB_GDT_CCT_CCT_NANOPB_H_INCLUDED #include +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.h" + /* @@protoc_insertion_point(includes) */ #if PB_PROTO_HEADER_VERSION != 30 #error Regenerate this file with the current version of nanopb generator. @@ -117,11 +119,21 @@ typedef struct _gdt_cct_IosClientInfo { /* @@protoc_insertion_point(struct:gdt_cct_IosClientInfo) */ } gdt_cct_IosClientInfo; +typedef struct _gdt_cct_MacClientInfo { + pb_bytes_array_t *os_major_version; + pb_bytes_array_t *os_full_version; + pb_bytes_array_t *application_build; + pb_bytes_array_t *application_bundle_id; +/* @@protoc_insertion_point(struct:gdt_cct_MacClientInfo) */ +} gdt_cct_MacClientInfo; + typedef struct _gdt_cct_ClientInfo { bool has_client_type; gdt_cct_ClientInfo_ClientType client_type; bool has_ios_client_info; gdt_cct_IosClientInfo ios_client_info; + bool has_mac_client_info; + gdt_cct_MacClientInfo mac_client_info; /* @@protoc_insertion_point(struct:gdt_cct_ClientInfo) */ } gdt_cct_ClientInfo; @@ -161,6 +173,8 @@ typedef struct _gdt_cct_LogEvent { int64_t event_uptime_ms; bool has_network_connection_info; gdt_cct_NetworkConnectionInfo network_connection_info; + bool has_compliance_data; + gdt_cct_ComplianceData compliance_data; /* @@protoc_insertion_point(struct:gdt_cct_LogEvent) */ } gdt_cct_LogEvent; @@ -195,19 +209,21 @@ extern const gdt_cct_QosTierConfiguration_QosTier gdt_cct_LogRequest_qos_tier_de extern const int32_t gdt_cct_QosTierConfiguration_log_source_default; /* Initializer values for message structs */ -#define gdt_cct_LogEvent_init_default {false, 0, NULL, false, 0, false, 0, false, 0, false, gdt_cct_NetworkConnectionInfo_init_default} +#define gdt_cct_LogEvent_init_default {false, 0, NULL, false, 0, false, 0, false, 0, false, gdt_cct_NetworkConnectionInfo_init_default, false, gdt_cct_ComplianceData_init_default} #define gdt_cct_NetworkConnectionInfo_init_default {false, gdt_cct_NetworkConnectionInfo_NetworkType_NONE, false, gdt_cct_NetworkConnectionInfo_MobileSubtype_UNKNOWN_MOBILE_SUBTYPE} +#define gdt_cct_MacClientInfo_init_default {NULL, NULL, NULL, NULL} #define gdt_cct_IosClientInfo_init_default {NULL, NULL, NULL, NULL, NULL, NULL, NULL} -#define gdt_cct_ClientInfo_init_default {false, _gdt_cct_ClientInfo_ClientType_MIN, false, gdt_cct_IosClientInfo_init_default} +#define gdt_cct_ClientInfo_init_default {false, _gdt_cct_ClientInfo_ClientType_MIN, false, gdt_cct_IosClientInfo_init_default, false, gdt_cct_MacClientInfo_init_default} #define gdt_cct_BatchedLogRequest_init_default {0, NULL} #define gdt_cct_LogRequest_init_default {false, gdt_cct_ClientInfo_init_default, false, 0, 0, NULL, false, 0, false, 0, false, gdt_cct_QosTierConfiguration_QosTier_DEFAULT} #define gdt_cct_QosTierConfiguration_init_default {false, _gdt_cct_QosTierConfiguration_QosTier_MIN, false, 0} #define gdt_cct_QosTiersOverride_init_default {0, NULL, false, 0} #define gdt_cct_LogResponse_init_default {false, 0, false, gdt_cct_QosTiersOverride_init_default} -#define gdt_cct_LogEvent_init_zero {false, 0, NULL, false, 0, false, 0, false, 0, false, gdt_cct_NetworkConnectionInfo_init_zero} +#define gdt_cct_LogEvent_init_zero {false, 0, NULL, false, 0, false, 0, false, 0, false, gdt_cct_NetworkConnectionInfo_init_zero, false, gdt_cct_ComplianceData_init_zero} #define gdt_cct_NetworkConnectionInfo_init_zero {false, _gdt_cct_NetworkConnectionInfo_NetworkType_MIN, false, _gdt_cct_NetworkConnectionInfo_MobileSubtype_MIN} +#define gdt_cct_MacClientInfo_init_zero {NULL, NULL, NULL, NULL} #define gdt_cct_IosClientInfo_init_zero {NULL, NULL, NULL, NULL, NULL, NULL, NULL} -#define gdt_cct_ClientInfo_init_zero {false, _gdt_cct_ClientInfo_ClientType_MIN, false, gdt_cct_IosClientInfo_init_zero} +#define gdt_cct_ClientInfo_init_zero {false, _gdt_cct_ClientInfo_ClientType_MIN, false, gdt_cct_IosClientInfo_init_zero, false, gdt_cct_MacClientInfo_init_zero} #define gdt_cct_BatchedLogRequest_init_zero {0, NULL} #define gdt_cct_LogRequest_init_zero {false, gdt_cct_ClientInfo_init_zero, false, 0, 0, NULL, false, 0, false, 0, false, _gdt_cct_QosTierConfiguration_QosTier_MIN} #define gdt_cct_QosTierConfiguration_init_zero {false, _gdt_cct_QosTierConfiguration_QosTier_MIN, false, 0} @@ -223,8 +239,13 @@ extern const int32_t gdt_cct_QosTierConfiguration_log_source_default; #define gdt_cct_IosClientInfo_model_tag 7 #define gdt_cct_IosClientInfo_language_code_tag 8 #define gdt_cct_IosClientInfo_application_bundle_id_tag 11 +#define gdt_cct_MacClientInfo_os_major_version_tag 1 +#define gdt_cct_MacClientInfo_os_full_version_tag 2 +#define gdt_cct_MacClientInfo_application_build_tag 3 +#define gdt_cct_MacClientInfo_application_bundle_id_tag 7 #define gdt_cct_ClientInfo_client_type_tag 1 #define gdt_cct_ClientInfo_ios_client_info_tag 4 +#define gdt_cct_ClientInfo_mac_client_info_tag 13 #define gdt_cct_NetworkConnectionInfo_network_type_tag 1 #define gdt_cct_NetworkConnectionInfo_mobile_subtype_tag 2 #define gdt_cct_QosTierConfiguration_qos_tier_tag 2 @@ -237,6 +258,7 @@ extern const int32_t gdt_cct_QosTierConfiguration_log_source_default; #define gdt_cct_LogEvent_source_extension_tag 6 #define gdt_cct_LogEvent_timezone_offset_seconds_tag 15 #define gdt_cct_LogEvent_network_connection_info_tag 23 +#define gdt_cct_LogEvent_compliance_data_tag 33 #define gdt_cct_LogRequest_request_time_ms_tag 4 #define gdt_cct_LogRequest_request_uptime_ms_tag 8 #define gdt_cct_LogRequest_client_info_tag 1 @@ -247,10 +269,11 @@ extern const int32_t gdt_cct_QosTierConfiguration_log_source_default; #define gdt_cct_LogResponse_qos_tier_tag 3 /* Struct field encoding specification for nanopb */ -extern const pb_field_t gdt_cct_LogEvent_fields[7]; +extern const pb_field_t gdt_cct_LogEvent_fields[8]; extern const pb_field_t gdt_cct_NetworkConnectionInfo_fields[3]; +extern const pb_field_t gdt_cct_MacClientInfo_fields[5]; extern const pb_field_t gdt_cct_IosClientInfo_fields[8]; -extern const pb_field_t gdt_cct_ClientInfo_fields[3]; +extern const pb_field_t gdt_cct_ClientInfo_fields[4]; extern const pb_field_t gdt_cct_BatchedLogRequest_fields[2]; extern const pb_field_t gdt_cct_LogRequest_fields[7]; extern const pb_field_t gdt_cct_QosTierConfiguration_fields[3]; @@ -260,6 +283,7 @@ extern const pb_field_t gdt_cct_LogResponse_fields[3]; /* Maximum encoded size of messages (where known) */ /* gdt_cct_LogEvent_size depends on runtime parameters */ #define gdt_cct_NetworkConnectionInfo_size 13 +/* gdt_cct_MacClientInfo_size depends on runtime parameters */ /* gdt_cct_IosClientInfo_size depends on runtime parameters */ /* gdt_cct_ClientInfo_size depends on runtime parameters */ /* gdt_cct_BatchedLogRequest_size depends on runtime parameters */ diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.c b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.c new file mode 100644 index 00000000..1e959545 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.c @@ -0,0 +1,92 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.3.9.9 */ + +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + + +const pb_field_t gdt_client_metrics_ClientMetrics_fields[5] = { + PB_FIELD( 1, MESSAGE , SINGULAR, STATIC , FIRST, gdt_client_metrics_ClientMetrics, window, window, &gdt_client_metrics_TimeWindow_fields), + PB_FIELD( 2, MESSAGE , REPEATED, POINTER , OTHER, gdt_client_metrics_ClientMetrics, log_source_metrics, window, &gdt_client_metrics_LogSourceMetrics_fields), + PB_FIELD( 3, MESSAGE , SINGULAR, STATIC , OTHER, gdt_client_metrics_ClientMetrics, global_metrics, log_source_metrics, &gdt_client_metrics_GlobalMetrics_fields), + PB_FIELD( 4, BYTES , SINGULAR, POINTER , OTHER, gdt_client_metrics_ClientMetrics, app_namespace, global_metrics, 0), + PB_LAST_FIELD +}; + +const pb_field_t gdt_client_metrics_TimeWindow_fields[3] = { + PB_FIELD( 1, INT64 , SINGULAR, STATIC , FIRST, gdt_client_metrics_TimeWindow, start_ms, start_ms, 0), + PB_FIELD( 2, INT64 , SINGULAR, STATIC , OTHER, gdt_client_metrics_TimeWindow, end_ms, start_ms, 0), + PB_LAST_FIELD +}; + +const pb_field_t gdt_client_metrics_GlobalMetrics_fields[2] = { + PB_FIELD( 1, MESSAGE , SINGULAR, STATIC , FIRST, gdt_client_metrics_GlobalMetrics, storage_metrics, storage_metrics, &gdt_client_metrics_StorageMetrics_fields), + PB_LAST_FIELD +}; + +const pb_field_t gdt_client_metrics_StorageMetrics_fields[3] = { + PB_FIELD( 1, INT64 , SINGULAR, STATIC , FIRST, gdt_client_metrics_StorageMetrics, current_cache_size_bytes, current_cache_size_bytes, 0), + PB_FIELD( 2, INT64 , SINGULAR, STATIC , OTHER, gdt_client_metrics_StorageMetrics, max_cache_size_bytes, current_cache_size_bytes, 0), + PB_LAST_FIELD +}; + +const pb_field_t gdt_client_metrics_LogSourceMetrics_fields[3] = { + PB_FIELD( 1, BYTES , SINGULAR, POINTER , FIRST, gdt_client_metrics_LogSourceMetrics, log_source, log_source, 0), + PB_FIELD( 2, MESSAGE , REPEATED, POINTER , OTHER, gdt_client_metrics_LogSourceMetrics, log_event_dropped, log_source, &gdt_client_metrics_LogEventDropped_fields), + PB_LAST_FIELD +}; + +const pb_field_t gdt_client_metrics_LogEventDropped_fields[3] = { + PB_FIELD( 1, INT64 , SINGULAR, STATIC , FIRST, gdt_client_metrics_LogEventDropped, events_dropped_count, events_dropped_count, 0), + PB_FIELD( 3, UENUM , SINGULAR, STATIC , OTHER, gdt_client_metrics_LogEventDropped, reason, events_dropped_count, 0), + PB_LAST_FIELD +}; + + + +/* Check that field information fits in pb_field_t */ +#if !defined(PB_FIELD_32BIT) +/* If you get an error here, it means that you need to define PB_FIELD_32BIT + * compile-time option. You can do that in pb.h or on compiler command line. + * + * The reason you need to do this is that some of your messages contain tag + * numbers or field sizes that are larger than what can fit in 8 or 16 bit + * field descriptors. + */ +PB_STATIC_ASSERT((pb_membersize(gdt_client_metrics_ClientMetrics, window) < 65536 && pb_membersize(gdt_client_metrics_ClientMetrics, global_metrics) < 65536 && pb_membersize(gdt_client_metrics_GlobalMetrics, storage_metrics) < 65536), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_gdt_client_metrics_ClientMetrics_gdt_client_metrics_TimeWindow_gdt_client_metrics_GlobalMetrics_gdt_client_metrics_StorageMetrics_gdt_client_metrics_LogSourceMetrics_gdt_client_metrics_LogEventDropped) +#endif + +#if !defined(PB_FIELD_16BIT) && !defined(PB_FIELD_32BIT) +/* If you get an error here, it means that you need to define PB_FIELD_16BIT + * compile-time option. You can do that in pb.h or on compiler command line. + * + * The reason you need to do this is that some of your messages contain tag + * numbers or field sizes that are larger than what can fit in the default + * 8 bit descriptors. + */ +PB_STATIC_ASSERT((pb_membersize(gdt_client_metrics_ClientMetrics, window) < 256 && pb_membersize(gdt_client_metrics_ClientMetrics, global_metrics) < 256 && pb_membersize(gdt_client_metrics_GlobalMetrics, storage_metrics) < 256), YOU_MUST_DEFINE_PB_FIELD_16BIT_FOR_MESSAGES_gdt_client_metrics_ClientMetrics_gdt_client_metrics_TimeWindow_gdt_client_metrics_GlobalMetrics_gdt_client_metrics_StorageMetrics_gdt_client_metrics_LogSourceMetrics_gdt_client_metrics_LogEventDropped) +#endif + + +/* @@protoc_insertion_point(eof) */ diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.h b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.h new file mode 100644 index 00000000..c4d76862 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.h @@ -0,0 +1,141 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.3.9.9 */ + +#ifndef PB_GDT_CLIENT_METRICS_CLIENT_METRICS_NANOPB_H_INCLUDED +#define PB_GDT_CLIENT_METRICS_CLIENT_METRICS_NANOPB_H_INCLUDED +#include + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + +/* Enum definitions */ +typedef enum _gdt_client_metrics_LogEventDropped_Reason { + gdt_client_metrics_LogEventDropped_Reason_REASON_UNKNOWN = 0, + gdt_client_metrics_LogEventDropped_Reason_MESSAGE_TOO_OLD = 1, + gdt_client_metrics_LogEventDropped_Reason_CACHE_FULL = 2, + gdt_client_metrics_LogEventDropped_Reason_PAYLOAD_TOO_BIG = 3, + gdt_client_metrics_LogEventDropped_Reason_MAX_RETRIES_REACHED = 4, + gdt_client_metrics_LogEventDropped_Reason_INVALID_PAYLOD = 5, + gdt_client_metrics_LogEventDropped_Reason_SERVER_ERROR = 6 +} gdt_client_metrics_LogEventDropped_Reason; +#define _gdt_client_metrics_LogEventDropped_Reason_MIN gdt_client_metrics_LogEventDropped_Reason_REASON_UNKNOWN +#define _gdt_client_metrics_LogEventDropped_Reason_MAX gdt_client_metrics_LogEventDropped_Reason_SERVER_ERROR +#define _gdt_client_metrics_LogEventDropped_Reason_ARRAYSIZE ((gdt_client_metrics_LogEventDropped_Reason)(gdt_client_metrics_LogEventDropped_Reason_SERVER_ERROR+1)) + +/* Struct definitions */ +typedef struct _gdt_client_metrics_LogSourceMetrics { + pb_bytes_array_t *log_source; + pb_size_t log_event_dropped_count; + struct _gdt_client_metrics_LogEventDropped *log_event_dropped; +/* @@protoc_insertion_point(struct:gdt_client_metrics_LogSourceMetrics) */ +} gdt_client_metrics_LogSourceMetrics; + +typedef struct _gdt_client_metrics_LogEventDropped { + int64_t events_dropped_count; + gdt_client_metrics_LogEventDropped_Reason reason; +/* @@protoc_insertion_point(struct:gdt_client_metrics_LogEventDropped) */ +} gdt_client_metrics_LogEventDropped; + +typedef struct _gdt_client_metrics_StorageMetrics { + int64_t current_cache_size_bytes; + int64_t max_cache_size_bytes; +/* @@protoc_insertion_point(struct:gdt_client_metrics_StorageMetrics) */ +} gdt_client_metrics_StorageMetrics; + +typedef struct _gdt_client_metrics_TimeWindow { + int64_t start_ms; + int64_t end_ms; +/* @@protoc_insertion_point(struct:gdt_client_metrics_TimeWindow) */ +} gdt_client_metrics_TimeWindow; + +typedef struct _gdt_client_metrics_GlobalMetrics { + gdt_client_metrics_StorageMetrics storage_metrics; +/* @@protoc_insertion_point(struct:gdt_client_metrics_GlobalMetrics) */ +} gdt_client_metrics_GlobalMetrics; + +typedef struct _gdt_client_metrics_ClientMetrics { + gdt_client_metrics_TimeWindow window; + pb_size_t log_source_metrics_count; + struct _gdt_client_metrics_LogSourceMetrics *log_source_metrics; + gdt_client_metrics_GlobalMetrics global_metrics; + pb_bytes_array_t *app_namespace; +/* @@protoc_insertion_point(struct:gdt_client_metrics_ClientMetrics) */ +} gdt_client_metrics_ClientMetrics; + +/* Default values for struct fields */ + +/* Initializer values for message structs */ +#define gdt_client_metrics_ClientMetrics_init_default {gdt_client_metrics_TimeWindow_init_default, 0, NULL, gdt_client_metrics_GlobalMetrics_init_default, NULL} +#define gdt_client_metrics_TimeWindow_init_default {0, 0} +#define gdt_client_metrics_GlobalMetrics_init_default {gdt_client_metrics_StorageMetrics_init_default} +#define gdt_client_metrics_StorageMetrics_init_default {0, 0} +#define gdt_client_metrics_LogSourceMetrics_init_default {NULL, 0, NULL} +#define gdt_client_metrics_LogEventDropped_init_default {0, _gdt_client_metrics_LogEventDropped_Reason_MIN} +#define gdt_client_metrics_ClientMetrics_init_zero {gdt_client_metrics_TimeWindow_init_zero, 0, NULL, gdt_client_metrics_GlobalMetrics_init_zero, NULL} +#define gdt_client_metrics_TimeWindow_init_zero {0, 0} +#define gdt_client_metrics_GlobalMetrics_init_zero {gdt_client_metrics_StorageMetrics_init_zero} +#define gdt_client_metrics_StorageMetrics_init_zero {0, 0} +#define gdt_client_metrics_LogSourceMetrics_init_zero {NULL, 0, NULL} +#define gdt_client_metrics_LogEventDropped_init_zero {0, _gdt_client_metrics_LogEventDropped_Reason_MIN} + +/* Field tags (for use in manual encoding/decoding) */ +#define gdt_client_metrics_LogSourceMetrics_log_source_tag 1 +#define gdt_client_metrics_LogSourceMetrics_log_event_dropped_tag 2 +#define gdt_client_metrics_LogEventDropped_events_dropped_count_tag 1 +#define gdt_client_metrics_LogEventDropped_reason_tag 3 +#define gdt_client_metrics_StorageMetrics_current_cache_size_bytes_tag 1 +#define gdt_client_metrics_StorageMetrics_max_cache_size_bytes_tag 2 +#define gdt_client_metrics_TimeWindow_start_ms_tag 1 +#define gdt_client_metrics_TimeWindow_end_ms_tag 2 +#define gdt_client_metrics_GlobalMetrics_storage_metrics_tag 1 +#define gdt_client_metrics_ClientMetrics_window_tag 1 +#define gdt_client_metrics_ClientMetrics_log_source_metrics_tag 2 +#define gdt_client_metrics_ClientMetrics_global_metrics_tag 3 +#define gdt_client_metrics_ClientMetrics_app_namespace_tag 4 + +/* Struct field encoding specification for nanopb */ +extern const pb_field_t gdt_client_metrics_ClientMetrics_fields[5]; +extern const pb_field_t gdt_client_metrics_TimeWindow_fields[3]; +extern const pb_field_t gdt_client_metrics_GlobalMetrics_fields[2]; +extern const pb_field_t gdt_client_metrics_StorageMetrics_fields[3]; +extern const pb_field_t gdt_client_metrics_LogSourceMetrics_fields[3]; +extern const pb_field_t gdt_client_metrics_LogEventDropped_fields[3]; + +/* Maximum encoded size of messages (where known) */ +/* gdt_client_metrics_ClientMetrics_size depends on runtime parameters */ +#define gdt_client_metrics_TimeWindow_size 22 +#define gdt_client_metrics_GlobalMetrics_size 24 +#define gdt_client_metrics_StorageMetrics_size 22 +/* gdt_client_metrics_LogSourceMetrics_size depends on runtime parameters */ +#define gdt_client_metrics_LogEventDropped_size 13 + +/* Message IDs (where set with "msgid" option) */ +#ifdef PB_MSGID + +#define CLIENT_METRICS_MESSAGES \ + + +#endif + +/* @@protoc_insertion_point(eof) */ + +#endif diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.c b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.c new file mode 100644 index 00000000..f3552f72 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.c @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.3.9.9 */ + +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + +const gdt_cct_ComplianceData_ProductIdOrigin gdt_cct_ComplianceData_product_id_origin_default = gdt_cct_ComplianceData_ProductIdOrigin_NOT_SET; + + +const pb_field_t gdt_cct_ComplianceData_fields[3] = { + PB_FIELD( 1, MESSAGE , OPTIONAL, STATIC , FIRST, gdt_cct_ComplianceData, privacy_context, privacy_context, &privacy_context_external_ExternalPrivacyContext_fields), + PB_FIELD( 2, UENUM , OPTIONAL, STATIC , OTHER, gdt_cct_ComplianceData, product_id_origin, privacy_context, &gdt_cct_ComplianceData_product_id_origin_default), + PB_LAST_FIELD +}; + + + +/* Check that field information fits in pb_field_t */ +#if !defined(PB_FIELD_32BIT) +/* If you get an error here, it means that you need to define PB_FIELD_32BIT + * compile-time option. You can do that in pb.h or on compiler command line. + * + * The reason you need to do this is that some of your messages contain tag + * numbers or field sizes that are larger than what can fit in 8 or 16 bit + * field descriptors. + */ +PB_STATIC_ASSERT((pb_membersize(gdt_cct_ComplianceData, privacy_context) < 65536), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_gdt_cct_ComplianceData) +#endif + +#if !defined(PB_FIELD_16BIT) && !defined(PB_FIELD_32BIT) +/* If you get an error here, it means that you need to define PB_FIELD_16BIT + * compile-time option. You can do that in pb.h or on compiler command line. + * + * The reason you need to do this is that some of your messages contain tag + * numbers or field sizes that are larger than what can fit in the default + * 8 bit descriptors. + */ +PB_STATIC_ASSERT((pb_membersize(gdt_cct_ComplianceData, privacy_context) < 256), YOU_MUST_DEFINE_PB_FIELD_16BIT_FOR_MESSAGES_gdt_cct_ComplianceData) +#endif + + +/* @@protoc_insertion_point(eof) */ diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.h b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.h new file mode 100644 index 00000000..a143c76e --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/compliance.nanopb.h @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.3.9.9 */ + +#ifndef PB_GDT_CCT_COMPLIANCE_NANOPB_H_INCLUDED +#define PB_GDT_CCT_COMPLIANCE_NANOPB_H_INCLUDED +#include + +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + +/* Enum definitions */ +typedef enum _gdt_cct_ComplianceData_ProductIdOrigin { + gdt_cct_ComplianceData_ProductIdOrigin_NOT_SET = 0, + gdt_cct_ComplianceData_ProductIdOrigin_EVENT_OVERRIDE = 5 +} gdt_cct_ComplianceData_ProductIdOrigin; +#define _gdt_cct_ComplianceData_ProductIdOrigin_MIN gdt_cct_ComplianceData_ProductIdOrigin_NOT_SET +#define _gdt_cct_ComplianceData_ProductIdOrigin_MAX gdt_cct_ComplianceData_ProductIdOrigin_EVENT_OVERRIDE +#define _gdt_cct_ComplianceData_ProductIdOrigin_ARRAYSIZE ((gdt_cct_ComplianceData_ProductIdOrigin)(gdt_cct_ComplianceData_ProductIdOrigin_EVENT_OVERRIDE+1)) + +/* Struct definitions */ +typedef struct _gdt_cct_ComplianceData { + bool has_privacy_context; + privacy_context_external_ExternalPrivacyContext privacy_context; + bool has_product_id_origin; + gdt_cct_ComplianceData_ProductIdOrigin product_id_origin; +/* @@protoc_insertion_point(struct:gdt_cct_ComplianceData) */ +} gdt_cct_ComplianceData; + +/* Default values for struct fields */ +extern const gdt_cct_ComplianceData_ProductIdOrigin gdt_cct_ComplianceData_product_id_origin_default; + +/* Initializer values for message structs */ +#define gdt_cct_ComplianceData_init_default {false, privacy_context_external_ExternalPrivacyContext_init_default, false, gdt_cct_ComplianceData_ProductIdOrigin_NOT_SET} +#define gdt_cct_ComplianceData_init_zero {false, privacy_context_external_ExternalPrivacyContext_init_zero, false, _gdt_cct_ComplianceData_ProductIdOrigin_MIN} + +/* Field tags (for use in manual encoding/decoding) */ +#define gdt_cct_ComplianceData_privacy_context_tag 1 +#define gdt_cct_ComplianceData_product_id_origin_tag 2 + +/* Struct field encoding specification for nanopb */ +extern const pb_field_t gdt_cct_ComplianceData_fields[3]; + +/* Maximum encoded size of messages (where known) */ +#define gdt_cct_ComplianceData_size (14 + privacy_context_external_ExternalPRequestContext_size) + +/* Message IDs (where set with "msgid" option) */ +#ifdef PB_MSGID + +#define COMPLIANCE_MESSAGES \ + + +#endif + +/* @@protoc_insertion_point(eof) */ + +#endif diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.c b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.c new file mode 100644 index 00000000..9eb1d167 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.c @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.3.9.9 */ + +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + + +const pb_field_t privacy_context_external_ExternalPRequestContext_fields[2] = { + PB_FIELD( 13, INT32 , OPTIONAL, STATIC , FIRST, privacy_context_external_ExternalPRequestContext, origin_associated_product_id, origin_associated_product_id, 0), + PB_LAST_FIELD +}; + + +/* @@protoc_insertion_point(eof) */ diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.h b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.h new file mode 100644 index 00000000..03f0f124 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.h @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.3.9.9 */ + +#ifndef PB_PRIVACY_CONTEXT_EXTERNAL_EXTERNAL_PREQUEST_CONTEXT_NANOPB_H_INCLUDED +#define PB_PRIVACY_CONTEXT_EXTERNAL_EXTERNAL_PREQUEST_CONTEXT_NANOPB_H_INCLUDED +#include + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + +/* Struct definitions */ +typedef struct _privacy_context_external_ExternalPRequestContext { + bool has_origin_associated_product_id; + int32_t origin_associated_product_id; +/* @@protoc_insertion_point(struct:privacy_context_external_ExternalPRequestContext) */ +} privacy_context_external_ExternalPRequestContext; + +/* Default values for struct fields */ + +/* Initializer values for message structs */ +#define privacy_context_external_ExternalPRequestContext_init_default {false, 0} +#define privacy_context_external_ExternalPRequestContext_init_zero {false, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define privacy_context_external_ExternalPRequestContext_origin_associated_product_id_tag 13 + +/* Struct field encoding specification for nanopb */ +extern const pb_field_t privacy_context_external_ExternalPRequestContext_fields[2]; + +/* Maximum encoded size of messages (where known) */ +#define privacy_context_external_ExternalPRequestContext_size 11 + +/* Message IDs (where set with "msgid" option) */ +#ifdef PB_MSGID + +#define EXTERNAL_PREQUEST_CONTEXT_MESSAGES \ + + +#endif + +/* @@protoc_insertion_point(eof) */ + +#endif diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.c b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.c new file mode 100644 index 00000000..8b816057 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.c @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.3.9.9 */ + +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + + +const pb_field_t privacy_context_external_ExternalPrivacyContext_fields[2] = { + PB_FIELD( 2, MESSAGE , OPTIONAL, STATIC , FIRST, privacy_context_external_ExternalPrivacyContext, prequest, prequest, &privacy_context_external_ExternalPRequestContext_fields), + PB_LAST_FIELD +}; + + +/* Check that field information fits in pb_field_t */ +#if !defined(PB_FIELD_32BIT) +/* If you get an error here, it means that you need to define PB_FIELD_32BIT + * compile-time option. You can do that in pb.h or on compiler command line. + * + * The reason you need to do this is that some of your messages contain tag + * numbers or field sizes that are larger than what can fit in 8 or 16 bit + * field descriptors. + */ +PB_STATIC_ASSERT((pb_membersize(privacy_context_external_ExternalPrivacyContext, prequest) < 65536), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_privacy_context_external_ExternalPrivacyContext) +#endif + +#if !defined(PB_FIELD_16BIT) && !defined(PB_FIELD_32BIT) +/* If you get an error here, it means that you need to define PB_FIELD_16BIT + * compile-time option. You can do that in pb.h or on compiler command line. + * + * The reason you need to do this is that some of your messages contain tag + * numbers or field sizes that are larger than what can fit in the default + * 8 bit descriptors. + */ +PB_STATIC_ASSERT((pb_membersize(privacy_context_external_ExternalPrivacyContext, prequest) < 256), YOU_MUST_DEFINE_PB_FIELD_16BIT_FOR_MESSAGES_privacy_context_external_ExternalPrivacyContext) +#endif + + +/* @@protoc_insertion_point(eof) */ diff --git a/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.h b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.h new file mode 100644 index 00000000..dc6981a5 --- /dev/null +++ b/GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_privacy_context.nanopb.h @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.3.9.9 */ + +#ifndef PB_PRIVACY_CONTEXT_EXTERNAL_EXTERNAL_PRIVACY_CONTEXT_NANOPB_H_INCLUDED +#define PB_PRIVACY_CONTEXT_EXTERNAL_EXTERNAL_PRIVACY_CONTEXT_NANOPB_H_INCLUDED +#include + +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/external_prequest_context.nanopb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + +/* Struct definitions */ +typedef struct _privacy_context_external_ExternalPrivacyContext { + bool has_prequest; + privacy_context_external_ExternalPRequestContext prequest; +/* @@protoc_insertion_point(struct:privacy_context_external_ExternalPrivacyContext) */ +} privacy_context_external_ExternalPrivacyContext; + +/* Default values for struct fields */ + +/* Initializer values for message structs */ +#define privacy_context_external_ExternalPrivacyContext_init_default {false, privacy_context_external_ExternalPRequestContext_init_default} +#define privacy_context_external_ExternalPrivacyContext_init_zero {false, privacy_context_external_ExternalPRequestContext_init_zero} + +/* Field tags (for use in manual encoding/decoding) */ +#define privacy_context_external_ExternalPrivacyContext_prequest_tag 2 + +/* Struct field encoding specification for nanopb */ +extern const pb_field_t privacy_context_external_ExternalPrivacyContext_fields[2]; + +/* Maximum encoded size of messages (where known) */ +#define privacy_context_external_ExternalPrivacyContext_size 13 + +/* Message IDs (where set with "msgid" option) */ +#ifdef PB_MSGID + +#define EXTERNAL_PRIVACY_CONTEXT_MESSAGES \ + + +#endif + +/* @@protoc_insertion_point(eof) */ + +#endif diff --git a/GoogleDataTransport/GDTCCTTestApp/app.swift b/GoogleDataTransport/GDTCCTTestApp/app.swift index cf5af7e8..4a48dce0 100644 --- a/GoogleDataTransport/GDTCCTTestApp/app.swift +++ b/GoogleDataTransport/GDTCCTTestApp/app.swift @@ -32,12 +32,21 @@ import GoogleDataTransport } public class ViewController: UIViewController { - let cctTransport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.CCT)! - let fllTransport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.FLL)! - let cshTransport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.CSH)! + let cctTransport = GDTCORTransport( + mappingID: "1018", + transformers: nil, + target: GDTCORTarget.CCT + )! + let fllTransport = GDTCORTransport( + mappingID: "1018", + transformers: nil, + target: GDTCORTarget.FLL + )! + let cshTransport = GDTCORTransport( + mappingID: "1018", + transformers: nil, + target: GDTCORTarget.CSH + )! @IBOutlet var backendSwitch: UISegmentedControl? @@ -97,12 +106,21 @@ import GoogleDataTransport } public class ViewController: NSViewController { - let cctTransport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.CCT)! - let fllTransport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.FLL)! - let cshTransport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.CSH)! + let cctTransport = GDTCORTransport( + mappingID: "1018", + transformers: nil, + target: GDTCORTarget.CCT + )! + let fllTransport = GDTCORTransport( + mappingID: "1018", + transformers: nil, + target: GDTCORTarget.FLL + )! + let cshTransport = GDTCORTransport( + mappingID: "1018", + transformers: nil, + target: GDTCORTarget.CSH + )! @IBOutlet var backendSwitch: NSSegmentedControl? diff --git a/GoogleDataTransport/GDTCCTTestApp/gdthelpers.swift b/GoogleDataTransport/GDTCCTTestApp/gdthelpers.swift index 118da3b0..40c7a758 100644 --- a/GoogleDataTransport/GDTCCTTestApp/gdthelpers.swift +++ b/GoogleDataTransport/GDTCCTTestApp/gdthelpers.swift @@ -17,7 +17,7 @@ import GoogleDataTransport class FirelogTestMessageHolder: NSObject, GDTCOREventDataObject { - public var root: FirelogTestMessage = FirelogTestMessage() + public var root: FirelogTestMessage = .init() func transportBytes() -> Data { do { diff --git a/GoogleDataTransport/GDTCCTTestApp/globals.swift b/GoogleDataTransport/GDTCCTTestApp/globals.swift index 4519686e..6a226a3d 100644 --- a/GoogleDataTransport/GDTCCTTestApp/globals.swift +++ b/GoogleDataTransport/GDTCCTTestApp/globals.swift @@ -16,7 +16,7 @@ import Foundation -public struct Globals { +public enum Globals { public static var SharedViewController: ViewController? public static var IsMonkeyTesting: Bool = false diff --git a/GoogleDataTransport/GDTCCTTestApp/viewcontroller.swift b/GoogleDataTransport/GDTCCTTestApp/viewcontroller.swift index dc40ba01..7b5355c3 100644 --- a/GoogleDataTransport/GDTCCTTestApp/viewcontroller.swift +++ b/GoogleDataTransport/GDTCCTTestApp/viewcontroller.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import Foundation import Dispatch +import Foundation import GoogleDataTransport public extension ViewController { @@ -122,7 +122,7 @@ public extension ViewController { print("Beginning monkey test") Globals.IsMonkeyTesting = true - let sema: DispatchSemaphore = DispatchSemaphore(value: 0) + let sema = DispatchSemaphore(value: 0) var generateEvents = true DispatchQueue.global().asyncAfter(deadline: .now() + Globals.MonkeyTestLength) { generateEvents = false @@ -138,7 +138,7 @@ public extension ViewController { self.generateWifiOnlyEvent, self.generateDailyEvent, ] - let randomIndex: Int = Int.random(in: 0 ..< generationFunctions.count) + let randomIndex: Int = .random(in: 0 ..< generationFunctions.count) generationFunctions[randomIndex](self) } RunLoop.current.run(until: Date(timeIntervalSinceNow: Double.random(in: 0 ..< 1.5))) diff --git a/GoogleDataTransport/GDTCCTTests/Common/TestStorage/GDTCCTTestStorage.m b/GoogleDataTransport/GDTCCTTests/Common/TestStorage/GDTCCTTestStorage.m index 288ba0a4..066fdeb3 100644 --- a/GoogleDataTransport/GDTCCTTests/Common/TestStorage/GDTCCTTestStorage.m +++ b/GoogleDataTransport/GDTCCTTests/Common/TestStorage/GDTCCTTestStorage.m @@ -33,6 +33,8 @@ @implementation GDTCCTTestStorage { NSMutableDictionary *> *_batches; } +@synthesize delegate = _delegate; + - (instancetype)init { self = [super init]; if (self) { @@ -207,6 +209,18 @@ - (void)defaultBatchWithEventSelector:(nonnull GDTCORStorageEventSelector *)even }]; } +- (FBLPromise *)fetchAndUpdateMetricsWithHandler: + (GDTCORMetricsMetadata * (^)(GDTCORMetricsMetadata *_Nullable fetchedMetadata, + NSError *_Nullable fetchError))handler { + NSAssert(NO, @"This API should be implemented if this fake is used in tests."); + return [FBLPromise resolvedWith:nil]; +} + +- (FBLPromise *)fetchStorageMetadata { + NSAssert(NO, @"This API should be implemented if this fake is used in tests."); + return [FBLPromise resolvedWith:nil]; +} + - (NSError *)genericRejectedPromiseErrorWithReason:(NSString *)reason { return [NSError errorWithDomain:@"GDTCORFlatFileStorage" code:-1 diff --git a/GoogleDataTransport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m b/GoogleDataTransport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m index 55af6461..3880f14a 100644 --- a/GoogleDataTransport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m +++ b/GoogleDataTransport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m @@ -24,24 +24,21 @@ #import #import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploader.h" -#import "GoogleDataTransport/GDTCCTTests/Unit/TestServer/GDTCCTTestServer.h" -#import "GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORFlatFileStorage+Testing.h" - -#import -#import -#import #import "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h" -@interface NSData (GDTCOREventDataObject) -@end +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsController.h" -@implementation NSData (GDTCOREventDataObject) +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.h" +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.h" +#import "GoogleDataTransport/GDTCCTTests/Unit/TestServer/GDTCCTTestServer.h" -- (NSData *)transportBytes { - return [self copy]; -} +#import "GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORFlatFileStorage+Testing.h" -@end +/// The mapping ID that represents the `LogSource` for GDT testing. +static NSString *const kTestEventMappingID = @"1018"; + +/// The mapping ID that represents the `LogSource` for GDT metrics. +static NSString *const kMetricEventMappingID = @"1710"; @interface GDTCCTIntegrationTest : XCTestCase /** If YES, allow the recursive generating of events. */ @@ -86,10 +83,6 @@ - (void)setUp { GDTCCTUploader.testServerURL = [self.testServer.serverURL URLByAppendingPathComponent:@"logBatch"]; - - self.transport = [[GDTCORTransport alloc] initWithMappingID:@"1018" - transformers:nil - target:kGDTCORTargetCSH]; } - (void)tearDown { @@ -99,6 +92,64 @@ - (void)tearDown { /** Tests sending data to CCT with a high priority event if network conditions are good. */ - (void)testSendingDataToCCT { + [self assertUploadToTarget:kGDTCORTargetCCT expectingMetrics:NO]; +} + +- (void)testRunsWithoutCrashingToCCT { + self.transport = [[GDTCORTransport alloc] initWithMappingID:kTestEventMappingID + transformers:nil + target:kGDTCORTargetCCT]; + // Just run for a minute whilst generating events. + NSInteger secondsToRun = 65; + self.generateEvents = YES; + + XCTestExpectation *eventsUploaded = [self expectationForEventsToUpload]; + eventsUploaded.expectedFulfillmentCount = 2; + + [self recursivelyGenerateEvent]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsToRun * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + self.generateEvents = NO; + + // Send a high priority event to flush other events. + [self generateEventWithQoSTier:GDTCOREventQoSFast]; + }); + + // Wait for events to be uploaded. + [self waitForExpectations:@[ eventsUploaded ] timeout:secondsToRun + 5]; + + // Validate sent events content. + [self assertAllScheduledEventsWereReceivedWhenExpectingMetrics:NO]; +} + +- (void)testSendingData_WhenTargetSupportsMetrics_AndThereAreMetricsToUpload { + [self assertUploadToTarget:kGDTCORTargetTest expectingMetrics:YES]; +} + +- (void)testSendingData_WhenTargetSupportsMetrics_AndThereAreNoMetricsToUpload { + [self assertUploadToTarget:kGDTCORTargetTest expectingMetrics:NO]; +} + +#pragma mark - Helpers + +/// Asserts that uploading a batch of events to the given target performs as expected. +/// @param target The target to upload the events to. +/// @param expectingMetrics Whether or not a metrics event should be uploaded to the target. +- (void)assertUploadToTarget:(GDTCORTarget)target expectingMetrics:(BOOL)expectingMetrics { + self.transport = [[GDTCORTransport alloc] initWithMappingID:kTestEventMappingID + transformers:nil + target:target]; + + if (expectingMetrics) { + [[GDTCORMetricsController sharedInstance] + logEventsDroppedForReason:GDTCOREventDropReasonStorageFull + events:[NSSet setWithArray:@[ + [self.transport eventForTransport], [self.transport eventForTransport], + [self.transport eventForTransport] + ]]]; + } + // Send a number of events across multiple queues in order to ensure the threading is working as // expected. dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); @@ -127,37 +178,10 @@ - (void)testSendingDataToCCT { [self waitForExpectations:@[ eventsUploaded ] timeout:60.0]; // Validate sent events content. - [self assertAllScheduledEventsWereReceived]; + [self assertAllScheduledEventsWereReceivedWhenExpectingMetrics:expectingMetrics]; } -- (void)testRunsWithoutCrashing { - // Just run for a minute whilst generating events. - NSInteger secondsToRun = 65; - self.generateEvents = YES; - - XCTestExpectation *eventsUploaded = [self expectationForEventsToUpload]; - eventsUploaded.expectedFulfillmentCount = 2; - - [self recursivelyGenerateEvent]; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsToRun * NSEC_PER_SEC)), - dispatch_get_main_queue(), ^{ - self.generateEvents = NO; - - // Send a high priority event to flush other events. - [self generateEventWithQoSTier:GDTCOREventQoSFast]; - }); - - // Wait for events to be uploaded. - [self waitForExpectations:@[ eventsUploaded ] timeout:secondsToRun + 5]; - - // Validate sent events content. - [self assertAllScheduledEventsWereReceived]; -} - -#pragma mark - Helpers - -/** Generates an event and sends it through the transport infrastructure. */ +/// Generates an event and sends it through the transport infrastructure. - (void)generateEventWithQoSTier:(GDTCOREventQoS)qosTier { GDTCOREvent *event = [self.transport eventForTransport]; event.dataObject = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding]; @@ -172,7 +196,7 @@ - (void)generateEventWithQoSTier:(GDTCOREventQoS)qosTier { }); } -/** Generates events recursively at random intervals between 0 and 5 seconds. */ +/// Generates events recursively at random intervals between 0 and 5 seconds. - (void)recursivelyGenerateEvent { if (self.generateEvents) { [self generateEventWithQoSTier:GDTCOREventQosDefault]; @@ -200,10 +224,10 @@ - (XCTestExpectation *)expectationForEventsToUpload { // Decode events. GCDWebServerDataRequest *dataRequest = (GCDWebServerDataRequest *)request; NSError *decodeError; - gdt_cct_BatchedLogRequest decodedRequest = [weakSelf requestWithData:dataRequest.data - error:&decodeError]; - __auto_type events = [weakSelf eventsWithBatchRequest:decodedRequest error:&decodeError]; + gdt_cct_BatchedLogRequest decodedRequest = + [GDTCCTTestRequestParser requestWithData:dataRequest.data error:&decodeError]; XCTAssertNil(decodeError); + __auto_type events = [GDTCCTTestRequestParser eventsWithBatchRequest:decodedRequest]; [weakSelf.serverReceivedEvents addObjectsFromArray:events]; // Send response. @@ -218,45 +242,25 @@ - (XCTestExpectation *)expectationForEventsToUpload { return responseSentExpectation; } -- (gdt_cct_BatchedLogRequest)requestWithData:(NSData *)data error:(NSError **)outError { - gdt_cct_BatchedLogRequest request = gdt_cct_BatchedLogRequest_init_default; - pb_istream_t istream = pb_istream_from_buffer([data bytes], [data length]); - if (!pb_decode(&istream, gdt_cct_BatchedLogRequest_fields, &request)) { - NSString *nanopb_error = [NSString stringWithFormat:@"%s", PB_GET_ERROR(&istream)]; - NSDictionary *userInfo = @{@"nanopb error:" : nanopb_error}; - if (outError != NULL) { - *outError = [NSError errorWithDomain:NSURLErrorDomain code:-1 userInfo:userInfo]; - } - request = (gdt_cct_BatchedLogRequest)gdt_cct_BatchedLogRequest_init_default; - } - return request; -} - -- (NSArray *)eventsWithBatchRequest:(gdt_cct_BatchedLogRequest)batchRequest - error:(NSError **)outError { - NSMutableArray *events = [NSMutableArray array]; - - for (NSUInteger reqIdx = 0; reqIdx < batchRequest.log_request_count; reqIdx++) { - gdt_cct_LogRequest request = batchRequest.log_request[reqIdx]; - - NSString *mappingID = @(request.log_source).stringValue; - - for (NSUInteger eventIdx = 0; eventIdx < request.log_event_count; eventIdx++) { - gdt_cct_LogEvent event = request.log_event[eventIdx]; - - GDTCOREvent *decodedEvent = [[GDTCOREvent alloc] initWithMappingID:mappingID - target:kGDTCORTargetTest]; - decodedEvent.dataObject = [NSData dataWithBytes:event.source_extension->bytes - length:event.source_extension->size]; - - [events addObject:decodedEvent]; +- (void)assertAllScheduledEventsWereReceivedWhenExpectingMetrics:(BOOL)expectingMetrics { + // Assert that a metrics event was sent only if it was expected. + NSInteger metricsEventCount = 0; + for (GDTCOREvent *receivedEvent in [self.serverReceivedEvents copy]) { + if ([receivedEvent.mappingID isEqualToString:kMetricEventMappingID]) { + // The event is a metrics event. + metricsEventCount += 1; + NSError *decodeError = nil; + gdt_client_metrics_ClientMetrics __unused decodedMetrics = + [GDTCCTTestRequestParser metricsProtoWithData:receivedEvent.serializedDataObjectBytes + error:&decodeError]; + XCTAssertNil(decodeError); + if (expectingMetrics) { + [self.scheduledEvents addObject:receivedEvent]; + } } } + XCTAssertEqual(metricsEventCount, @(expectingMetrics).integerValue); - return [events copy]; -} - -- (void)assertAllScheduledEventsWereReceived { // Assume unique payload. __auto_type scheduledEventsByPayload = [self eventsByPayloadWithEvents:[self.scheduledEvents copy]]; diff --git a/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m b/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m index a63e70d7..9e4e2ca8 100644 --- a/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m +++ b/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m @@ -73,6 +73,26 @@ - (void)testConstructBatchedLogRequest { pb_release(gdt_cct_BatchedLogRequest_fields, &batch); } +/** Tests batched log requests include platform-specific client info. */ +- (void)testBatchedLogRequestsIncludePlatformSpecificClientInfo { + // Given + gdt_cct_BatchedLogRequest batch = gdt_cct_BatchedLogRequest_init_default; + NSSet *events = + [NSSet setWithArray:[self.generator generateTheFiveConsistentEvents]]; + // When + XCTAssertNoThrow(batch = GDTCCTConstructBatchedLogRequest(@{@"1018" : events})); + // Then + XCTAssertTrue(batch.log_request->has_client_info); +#if TARGET_OS_IOS || TARGET_OS_TV + XCTAssertTrue(batch.log_request->client_info.has_ios_client_info); + XCTAssertFalse(batch.log_request->client_info.has_mac_client_info); +#elif TARGET_OS_OSX + XCTAssertTrue(batch.log_request->client_info.has_mac_client_info); + XCTAssertFalse(batch.log_request->client_info.has_ios_client_info); +#endif + pb_release(gdt_cct_BatchedLogRequest_fields, &batch); +} + /** Tests encoding a batched log request generates bytes equivalent to canonical protobuf. */ - (void)testEncodeBatchedLogRequest { NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; @@ -119,6 +139,20 @@ - (void)testBytesAreDecodable { pb_release(gdt_cct_BatchedLogRequest_fields, &decodedBatch); } +- (void)testLogEventsPopulateComplianceFieldWhenGDTEventHasProductData { + // Two of the generated events have product data. + NSSet *storedEvents = + [NSSet setWithArray:[self.generator generateTheFiveConsistentEvents]]; + gdt_cct_BatchedLogRequest batch = GDTCCTConstructBatchedLogRequest(@{@"1018" : storedEvents}); + int eventsThatContainProductData = 0; + for (int i = 0; i < batch.log_request->log_event_count; i++) { + gdt_cct_LogEvent logEvent = batch.log_request->log_event[i]; + eventsThatContainProductData += logEvent.has_compliance_data; + } + XCTAssertEqual(eventsThatContainProductData, 2, + @"Only two of the five events should have compliance data."); +} + - (void)testDecodedEventTimestampMatchToBatchContent { GDTCOREvent *storedEvent = [self.generator generateEvent:GDTCOREventQoSDaily]; NSSet *storedEvents = [NSSet setWithObject:storedEvent]; diff --git a/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTUploaderTest.m b/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTUploaderTest.m index c2c8bbba..e4d2a580 100644 --- a/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTUploaderTest.m +++ b/GoogleDataTransport/GDTCCTTests/Unit/GDTCCTUploaderTest.m @@ -16,13 +16,17 @@ #import +#import "FBLPromise+Testing.h" + #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h" #import "GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h" #import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h" #import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploader.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" #import "GoogleDataTransport/GDTCCTTests/Common/TestStorage/GDTCCTTestStorage.h" +#import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.h" #import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h" #import "GoogleDataTransport/GDTCCTTests/Unit/TestServer/GDTCCTTestServer.h" @@ -393,6 +397,384 @@ - (void)testUploadTarget_WhenThereAreNoEventsFirstThenEventsAdded_ThenUploadNewE [self waitForUploadOperationsToFinish:self.uploader]; } +- (void)testUploadTarget_WhenTargetSupportsMetricsCollection_ThenMetricsAreAddedToUploadBatch { + // Given + // - Generate and batch a test event. + [self.generator generateEvent:GDTCOREventQoSFast]; + [self batchEvents]; + + // - Set storage expectations. + [self setUpStorageExpectations]; + + XCTestExpectation *hasEventsExpectation = [self expectStorageHasEventsForTarget:kGDTCORTargetTest + result:YES]; + + // - Set metrics controller expectations. + GDTCORMetricsControllerFake *metricsControllerFake = [[GDTCORMetricsControllerFake alloc] init]; + [[GDTCORRegistrar sharedInstance] registerMetricsController:metricsControllerFake + target:kGDTCORTargetTest]; + + GDTCORMetrics *dummyMetrics = [[GDTCORMetrics alloc] init]; + XCTestExpectation *getAndResetMetricsExpectation = + [self expectationWithDescription:@"getAndResetMetricsExpectation"]; + metricsControllerFake.onGetAndResetMetricsHandler = ^FBLPromise * { + // This should not be fulfilled. + [getAndResetMetricsExpectation fulfill]; + return [FBLPromise resolvedWith:dummyMetrics]; + }; + + XCTestExpectation *offerMetricsExpectation = + [self expectationWithDescription:@"offerMetricsExpectation"]; + offerMetricsExpectation.inverted = YES; + metricsControllerFake.onOfferMetricsHandler = ^(GDTCORMetrics *__unused _) { + // This should not be fulfilled. + [offerMetricsExpectation fulfill]; + }; + + XCTestExpectation *droppedEventsAreLoggedExpectation = + [self expectationWithDescription:@"droppedEventsAreLoggedExpectation"]; + droppedEventsAreLoggedExpectation.inverted = YES; + metricsControllerFake.onLogEventsDroppedHandler = + ^(GDTCOREventDropReason __unused _, NSSet *__unused __) { + // This should not be fulfilled. + [droppedEventsAreLoggedExpectation fulfill]; + }; + + // - Set response expectations. + XCTestExpectation *responseSentExpectation = [self expectationTestServerSuccessRequestResponse]; + + // When + [self.uploader uploadTarget:kGDTCORTargetTest withConditions:GDTCORUploadConditionWifiData]; + + // Then + [self waitForExpectations:@[ + self.testStorage.batchIDsForTargetExpectation, + self.testStorage.removeBatchWithoutDeletingEventsExpectation, hasEventsExpectation, + self.testStorage.batchWithEventSelectorExpectation, getAndResetMetricsExpectation, + responseSentExpectation, offerMetricsExpectation, droppedEventsAreLoggedExpectation, + self.testStorage.removeBatchAndDeleteEventsExpectation + ] + timeout:1 + enforceOrder:YES]; + [self waitForUploadOperationsToFinish:self.uploader]; +} + +- (void)testUploadTarget_WhenTargetDoesNotSupportMetricsCollection_ThenNoMetricsAreSentOrLogged { + // Given + // - Generate and batch a test event. + [self.generator generateEvent:GDTCOREventQoSFast]; + [self batchEvents]; + + // - Set storage expectations. + [self setUpStorageExpectations]; + + XCTestExpectation *hasEventsExpectation = [self expectStorageHasEventsForTarget:kGDTCORTargetTest + result:YES]; + + // - Set metrics controller expectations. + GDTCORMetricsControllerFake *metricsControllerFake = [[GDTCORMetricsControllerFake alloc] init]; + // The `kGDTCORTargetTest` target is registered at +load time. De-registering + // it here will simulate the scenario where an upload target does not have a + // corresponding metrics controller (and therefore does not support metrics + // collection). + [[[GDTCORRegistrar sharedInstance] targetToMetricsController] + removeObjectForKey:@(kGDTCORTargetTest)]; + + XCTestExpectation *getAndResetExpectation = + [self expectationWithDescription:@"getAndResetExpectation"]; + getAndResetExpectation.inverted = YES; + metricsControllerFake.onGetAndResetMetricsHandler = ^FBLPromise * { + // This should not be fulfilled. + [getAndResetExpectation fulfill]; + return [FBLPromise resolvedWith:nil]; + }; + + XCTestExpectation *offerMetricsExpectation = + [self expectationWithDescription:@"offerMetricsExpectation"]; + offerMetricsExpectation.inverted = YES; + metricsControllerFake.onOfferMetricsHandler = ^(GDTCORMetrics *__unused _) { + // This should not be fulfilled. + [offerMetricsExpectation fulfill]; + }; + + XCTestExpectation *droppedEventsAreLoggedExpectation = + [self expectationWithDescription:@"droppedEventsAreLoggedExpectation"]; + droppedEventsAreLoggedExpectation.inverted = YES; + metricsControllerFake.onLogEventsDroppedHandler = + ^(GDTCOREventDropReason __unused _, NSSet *__unused __) { + // This should not be fulfilled. + [droppedEventsAreLoggedExpectation fulfill]; + }; + + // - Set response expectations. + XCTestExpectation *responseSentExpectation = [self expectationTestServerSuccessRequestResponse]; + + // When + [self.uploader uploadTarget:kGDTCORTargetTest withConditions:GDTCORUploadConditionWifiData]; + + // Then + [self waitForExpectations:@[ + self.testStorage.batchIDsForTargetExpectation, + self.testStorage.removeBatchWithoutDeletingEventsExpectation, hasEventsExpectation, + self.testStorage.batchWithEventSelectorExpectation, getAndResetExpectation, + responseSentExpectation, offerMetricsExpectation, droppedEventsAreLoggedExpectation, + self.testStorage.removeBatchAndDeleteEventsExpectation + ] + timeout:1 + enforceOrder:YES]; + [self waitForUploadOperationsToFinish:self.uploader]; +} + +- (void)testUploadTarget_WhenFailureToAddMetricsToBatch_DoesNotImpedeBatchUpload { + // Given + // - Generate and batch a test event. + [self.generator generateEvent:GDTCOREventQoSFast]; + [self batchEvents]; + + // - Set storage expectations. + [self setUpStorageExpectations]; + + XCTestExpectation *hasEventsExpectation = [self expectStorageHasEventsForTarget:kGDTCORTargetTest + result:YES]; + + // - Set metrics controller expectations. + GDTCORMetricsControllerFake *metricsControllerFake = [[GDTCORMetricsControllerFake alloc] init]; + [[GDTCORRegistrar sharedInstance] registerMetricsController:metricsControllerFake + target:kGDTCORTargetTest]; + + NSError *error = [NSError errorWithDomain:@"metrics controller error" code:1 userInfo:nil]; + XCTestExpectation *getAndResetExpectation = + [self expectationWithDescription:@"getAndResetExpectation"]; + metricsControllerFake.onGetAndResetMetricsHandler = ^FBLPromise * { + [getAndResetExpectation fulfill]; + return [FBLPromise resolvedWith:error]; + }; + + XCTestExpectation *offerMetricsExpectation = + [self expectationWithDescription:@"offerMetricsExpectation"]; + offerMetricsExpectation.inverted = YES; + metricsControllerFake.onOfferMetricsHandler = ^(GDTCORMetrics *__unused _) { + // This should not be fulfilled. + [offerMetricsExpectation fulfill]; + }; + + XCTestExpectation *droppedEventsAreLoggedExpectation = + [self expectationWithDescription:@"droppedEventsAreLoggedExpectation"]; + droppedEventsAreLoggedExpectation.inverted = YES; + metricsControllerFake.onLogEventsDroppedHandler = + ^(GDTCOREventDropReason __unused _, NSSet *__unused __) { + // This should not be fulfilled. + [droppedEventsAreLoggedExpectation fulfill]; + }; + + // - Set response expectations. + XCTestExpectation *responseSentExpectation = [self expectationTestServerSuccessRequestResponse]; + + // When + [self.uploader uploadTarget:kGDTCORTargetTest withConditions:GDTCORUploadConditionWifiData]; + + // Then + [self waitForExpectations:@[ + self.testStorage.batchIDsForTargetExpectation, + self.testStorage.removeBatchWithoutDeletingEventsExpectation, hasEventsExpectation, + self.testStorage.batchWithEventSelectorExpectation, getAndResetExpectation, + responseSentExpectation, offerMetricsExpectation, droppedEventsAreLoggedExpectation, + self.testStorage.removeBatchAndDeleteEventsExpectation + ] + timeout:1 + enforceOrder:YES]; + [self waitForUploadOperationsToFinish:self.uploader]; +} + +- (void)testUploadTarget_WhenBatchWithMetricsAreUploaded { + // Given + // - Generate and batch a test event. + [self.generator generateEvent:GDTCOREventQoSFast]; + [self batchEvents]; + + // - Set storage expectations. + [self setUpStorageExpectations]; + + XCTestExpectation *hasEventsExpectation = [self expectStorageHasEventsForTarget:kGDTCORTargetTest + result:YES]; + + // - Set metrics controller expectations. + GDTCORMetricsControllerFake *metricsControllerFake = [[GDTCORMetricsControllerFake alloc] init]; + [[GDTCORRegistrar sharedInstance] registerMetricsController:metricsControllerFake + target:kGDTCORTargetTest]; + + GDTCORMetrics *dummyMetrics = [[GDTCORMetrics alloc] init]; + XCTestExpectation *getAndResetExpectation = + [self expectationWithDescription:@"getAndResetExpectation"]; + metricsControllerFake.onGetAndResetMetricsHandler = ^FBLPromise * { + [getAndResetExpectation fulfill]; + return [FBLPromise resolvedWith:dummyMetrics]; + }; + + XCTestExpectation *offerMetricsExpectation = + [self expectationWithDescription:@"offerMetricsExpectation"]; + offerMetricsExpectation.inverted = YES; + metricsControllerFake.onOfferMetricsHandler = ^(GDTCORMetrics *__unused _) { + // This should not be fulfilled. + [offerMetricsExpectation fulfill]; + }; + + XCTestExpectation *droppedEventsAreLoggedExpectation = + [self expectationWithDescription:@"droppedEventsAreLoggedExpectation"]; + droppedEventsAreLoggedExpectation.inverted = YES; + metricsControllerFake.onLogEventsDroppedHandler = + ^(GDTCOREventDropReason __unused _, NSSet *__unused __) { + // This should not be fulfilled. + [droppedEventsAreLoggedExpectation fulfill]; + }; + + // - Set response expectations. + XCTestExpectation *responseSentExpectation = [self expectationTestServerSuccessRequestResponse]; + + // When + [self.uploader uploadTarget:kGDTCORTargetTest withConditions:GDTCORUploadConditionWifiData]; + + // Then + [self waitForExpectations:@[ + self.testStorage.batchIDsForTargetExpectation, + self.testStorage.removeBatchWithoutDeletingEventsExpectation, hasEventsExpectation, + self.testStorage.batchWithEventSelectorExpectation, getAndResetExpectation, + responseSentExpectation, offerMetricsExpectation, droppedEventsAreLoggedExpectation, + self.testStorage.removeBatchAndDeleteEventsExpectation + ] + timeout:1 + enforceOrder:YES]; + [self waitForUploadOperationsToFinish:self.uploader]; +} + +- (void) + testUploadTarget_WhenBatchWithMetricsAreNotUploadedDueToTransientError_ThenMetricsAreReStored { + // Given + // - Generate a test event. + [self.generator generateEvent:GDTCOREventQoSFast]; + + // - Set storage expectations. + [self setUpStorageExpectations]; + + self.testStorage.removeBatchAndDeleteEventsExpectation.inverted = YES; + self.testStorage.removeBatchWithoutDeletingEventsExpectation.inverted = NO; + + XCTestExpectation *hasEventsExpectation = [self expectStorageHasEventsForTarget:kGDTCORTargetTest + result:YES]; + + // - Set metrics controller expectations. + GDTCORMetricsControllerFake *metricsControllerFake = [[GDTCORMetricsControllerFake alloc] init]; + [[GDTCORRegistrar sharedInstance] registerMetricsController:metricsControllerFake + target:kGDTCORTargetTest]; + + GDTCORMetrics *dummyMetrics = [[GDTCORMetrics alloc] init]; + XCTestExpectation *getAndResetExpectation = + [self expectationWithDescription:@"getAndResetExpectation"]; + metricsControllerFake.onGetAndResetMetricsHandler = ^FBLPromise * { + [getAndResetExpectation fulfill]; + return [FBLPromise resolvedWith:dummyMetrics]; + }; + + XCTestExpectation *offerMetricsExpectation = + [self expectationWithDescription:@"offerMetricsExpectation"]; + metricsControllerFake.onOfferMetricsHandler = ^(GDTCORMetrics *metrics) { + [offerMetricsExpectation fulfill]; + XCTAssertEqualObjects(metrics, dummyMetrics); + }; + + XCTestExpectation *droppedEventsAreLoggedExpectation = + [self expectationWithDescription:@"droppedEventsAreLoggedExpectation"]; + droppedEventsAreLoggedExpectation.inverted = YES; + metricsControllerFake.onLogEventsDroppedHandler = + ^(GDTCOREventDropReason __unused _, NSSet *__unused __) { + // This should not be fulfilled. + [droppedEventsAreLoggedExpectation fulfill]; + }; + + // - Set response expectations. + XCTestExpectation *responseFailedExpectation = [self expectationTestServerResponseWithCode:500 + headers:nil]; + + // When + [self.uploader uploadTarget:kGDTCORTargetTest withConditions:GDTCORUploadConditionWifiData]; + + // Then + [self waitForExpectations:@[ + self.testStorage.batchIDsForTargetExpectation, hasEventsExpectation, + self.testStorage.batchWithEventSelectorExpectation, getAndResetExpectation, + responseFailedExpectation, offerMetricsExpectation, droppedEventsAreLoggedExpectation, + self.testStorage.removeBatchWithoutDeletingEventsExpectation, + self.testStorage.removeBatchAndDeleteEventsExpectation + ] + timeout:1 + enforceOrder:YES]; + [self waitForUploadOperationsToFinish:self.uploader]; +} + +- (void) + testUploadTarget_WhenBatchWithMetricsAreNotUploadedDueToNonTransientError_ThenMetricsAreReStoredAndDroppedBatchEventAreLogged { + // Given + // - Generate a test event. + [self.generator generateEvent:GDTCOREventQoSFast]; + + // - Set storage expectations. + [self setUpStorageExpectations]; + + self.testStorage.removeBatchAndDeleteEventsExpectation.inverted = NO; + self.testStorage.removeBatchWithoutDeletingEventsExpectation.inverted = YES; + + XCTestExpectation *hasEventsExpectation = [self expectStorageHasEventsForTarget:kGDTCORTargetTest + result:YES]; + + // - Set metrics controller expectations. + GDTCORMetricsControllerFake *metricsControllerFake = [[GDTCORMetricsControllerFake alloc] init]; + [[GDTCORRegistrar sharedInstance] registerMetricsController:metricsControllerFake + target:kGDTCORTargetTest]; + + GDTCORMetrics *dummyMetrics = [[GDTCORMetrics alloc] init]; + XCTestExpectation *getAndResetExpectation = + [self expectationWithDescription:@"getAndResetExpectation"]; + metricsControllerFake.onGetAndResetMetricsHandler = ^FBLPromise * { + [getAndResetExpectation fulfill]; + return [FBLPromise resolvedWith:dummyMetrics]; + }; + + XCTestExpectation *offerMetricsExpectation = + [self expectationWithDescription:@"offerMetricsExpectation"]; + metricsControllerFake.onOfferMetricsHandler = ^(GDTCORMetrics *metrics) { + [offerMetricsExpectation fulfill]; + XCTAssertEqualObjects(metrics, dummyMetrics); + }; + + XCTestExpectation *droppedEventsAreLoggedExpectation = + [self expectationWithDescription:@"droppedEventsAreLoggedExpectation"]; + metricsControllerFake.onLogEventsDroppedHandler = + ^(GDTCOREventDropReason reason, NSSet *droppedEvents) { + [droppedEventsAreLoggedExpectation fulfill]; + XCTAssertEqual(reason, GDTCOREventDropReasonInvalidPayload); + XCTAssertEqual([droppedEvents count], 2); + }; + + // - Set response expectations. + XCTestExpectation *responseFailedExpectation = [self expectationTestServerResponseWithCode:400 + headers:nil]; + + // When + [self.uploader uploadTarget:kGDTCORTargetTest withConditions:GDTCORUploadConditionWifiData]; + + // Then + [self waitForExpectations:@[ + self.testStorage.batchIDsForTargetExpectation, hasEventsExpectation, + self.testStorage.batchWithEventSelectorExpectation, getAndResetExpectation, + responseFailedExpectation, offerMetricsExpectation, droppedEventsAreLoggedExpectation, + self.testStorage.removeBatchWithoutDeletingEventsExpectation, + self.testStorage.removeBatchAndDeleteEventsExpectation + ] + timeout:1 + enforceOrder:YES]; + [self waitForUploadOperationsToFinish:self.uploader]; +} + #pragma mark - Storage interaction tests - (void)testStorageSelectorWhenConditionsHighPriority { @@ -855,7 +1237,7 @@ - (nullable NSURL *)serverURLForTarget:(GDTCORTarget)target { // fool github URL scanning bots. static NSURL *CCTServerURL; static NSString *const kINTServerURL = - @"https://dummyapiverylong-dummy.dummy.com/dummy/api/very/long"; + @"https://dummyapiverylong-dummy.google.com/dummy/api/very/long"; static dispatch_once_t CCTOnceToken; dispatch_once(&CCTOnceToken, ^{ const char *p1 = "hts/frbslgiggolai.o/0clgbth"; diff --git a/GoogleDataTransport/GDTCCTTests/Unit/GDTCORMetrics+GDTCCTSupportTests.m b/GoogleDataTransport/GDTCCTTests/Unit/GDTCORMetrics+GDTCCTSupportTests.m new file mode 100644 index 00000000..db694c74 --- /dev/null +++ b/GoogleDataTransport/GDTCCTTests/Unit/GDTCORMetrics+GDTCCTSupportTests.m @@ -0,0 +1,63 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCORMetrics+GDTCCTSupport.h" + +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.h" +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.h" + +@interface GDTCORMetrics_GDTCCTSupportTests : XCTestCase +@end + +@implementation GDTCORMetrics_GDTCCTSupportTests + +- (void)testTransportBytes { + // Given + GDTCORMetrics *metrics = [GDTCORMetricsTestHelpers metricsWithGeneratedDroppedEventCounters]; + + // When + NSData *metricsProtoData = [metrics transportBytes]; + XCTAssertNotNil(metricsProtoData); + XCTAssertGreaterThan(metricsProtoData.length, 0); + + // Then + NSError *decodeError = nil; + gdt_client_metrics_ClientMetrics metricsProto = + [GDTCCTTestRequestParser metricsProtoWithData:metricsProtoData error:&decodeError]; + XCTAssertNil(decodeError); + + [GDTCORMetricsTestHelpers assertMetrics:metrics correspondToProto:metricsProto]; +} + +- (void)testTransportBytes_WhenMetricsContainNoDroppedEventData { + // Given + GDTCORMetrics *metrics = [GDTCORMetricsTestHelpers metricsWithEmptyDroppedEventCounters]; + + // When + NSData *metricsProtoData = [metrics transportBytes]; + XCTAssertNotNil(metricsProtoData); + XCTAssertGreaterThan(metricsProtoData.length, 0); + + // Then + NSError *decodeError = nil; + gdt_client_metrics_ClientMetrics metricsProto = + [GDTCCTTestRequestParser metricsProtoWithData:metricsProtoData error:&decodeError]; + XCTAssertNil(decodeError); + + [GDTCORMetricsTestHelpers assertMetrics:metrics correspondToProto:metricsProto]; +} + +@end diff --git a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m index 932bc0a0..ee045fbe 100644 --- a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m +++ b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m @@ -20,6 +20,7 @@ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventDataObject.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h" #import "GoogleDataTransport/GDTCCTLibrary/Public/GDTCOREvent+GDTCCTSupport.h" @@ -110,10 +111,15 @@ - (NSURL *)writeConsistentMessageToDisk:(NSString *)messageResource { return [NSURL fileURLWithPath:filePath]; } +/// Consistently generates the same five events. The events slightly differ in their attributes. The +/// first two out of the five events have non-`nil` `productData`. - (NSArray *)generateTheFiveConsistentEvents { NSMutableArray *events = [[NSMutableArray alloc] init]; { - GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + GDTCOREvent *event = + [[GDTCOREvent alloc] initWithMappingID:@"1018" + productData:[[GDTCORProductData alloc] initWithProductID:98765] + target:_target]; event.clockSnapshot = [GDTCORClock snapshot]; [event.clockSnapshot setValue:@(1111111111111) forKeyPath:@"timeMillis"]; [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; @@ -143,7 +149,10 @@ - (NSURL *)writeConsistentMessageToDisk:(NSString *)messageResource { } { - GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + GDTCOREvent *event = + [[GDTCOREvent alloc] initWithMappingID:@"1018" + productData:[[GDTCORProductData alloc] initWithProductID:98765] + target:_target]; event.clockSnapshot = [GDTCORClock snapshot]; [event.clockSnapshot setValue:@(1111111111111) forKeyPath:@"timeMillis"]; [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; diff --git a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.h b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.h new file mode 100644 index 00000000..3b04f295 --- /dev/null +++ b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.h @@ -0,0 +1,49 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" + +#import "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h" +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A test utility class for parsing data or protos to their corresponding type. +@interface GDTCCTTestRequestParser : NSObject + +/// Parses the batched log request proto from the provided data. +/// @param data The given data to parse. +/// @param outError If the return value is `nil`, an ``NSError`` indicating why the parsing +/// operation failed. +/// @return An instance of ``gdt_cct_BatchedLogRequest``. ++ (gdt_cct_BatchedLogRequest)requestWithData:(NSData *)data error:(NSError **)outError; + +/// Parses the client metrics proto from the provided data. +/// @param data The given data to parse. +/// @param outError If the return value is `nil`, an ``NSError`` indicating why the parsing +/// operation failed. +/// @return An instance of ``gdt_client_metrics_ClientMetrics``. The instance will have default +/// values in the case of an error (check `outError`). ++ (gdt_client_metrics_ClientMetrics)metricsProtoWithData:(NSData *)data error:(NSError **)outError; + +/// Parses the ``GDTCOREvent``s used in the given batch log request proto. +/// @param batchRequest The given proto to parse. +/// @return An array of ``GDTCOREvent`` objects from the request. ++ (NSArray *)eventsWithBatchRequest:(gdt_cct_BatchedLogRequest)batchRequest; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.m b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.m new file mode 100644 index 00000000..0e641b1d --- /dev/null +++ b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.m @@ -0,0 +1,78 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCCTTestRequestParser.h" + +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.h" + +#import +#import +#import + +@implementation GDTCCTTestRequestParser + ++ (gdt_cct_BatchedLogRequest)requestWithData:(NSData *)data error:(NSError **)outError { + gdt_cct_BatchedLogRequest request = gdt_cct_BatchedLogRequest_init_default; + pb_istream_t istream = pb_istream_from_buffer([data bytes], [data length]); + if (!pb_decode(&istream, gdt_cct_BatchedLogRequest_fields, &request)) { + if (outError != NULL) { + NSString *nanopbError = [NSString stringWithFormat:@"%s", PB_GET_ERROR(&istream)]; + *outError = [NSError errorWithDomain:NSStringFromClass(self) + code:-1 + userInfo:@{@"nanopb error" : nanopbError}]; + } + } + return request; +} + ++ (NSArray *)eventsWithBatchRequest:(gdt_cct_BatchedLogRequest)batchRequest { + NSMutableArray *events = [NSMutableArray array]; + + for (NSUInteger reqIdx = 0; reqIdx < batchRequest.log_request_count; reqIdx++) { + gdt_cct_LogRequest request = batchRequest.log_request[reqIdx]; + + NSString *mappingID = @(request.log_source).stringValue; + + for (NSUInteger eventIdx = 0; eventIdx < request.log_event_count; eventIdx++) { + gdt_cct_LogEvent event = request.log_event[eventIdx]; + + GDTCOREvent *decodedEvent = [[GDTCOREvent alloc] initWithMappingID:mappingID + target:kGDTCORTargetTest]; + decodedEvent.dataObject = [NSData dataWithBytes:event.source_extension->bytes + length:event.source_extension->size]; + + [events addObject:decodedEvent]; + } + } + + return [events copy]; +} + ++ (gdt_client_metrics_ClientMetrics)metricsProtoWithData:(NSData *)data error:(NSError **)outError { + gdt_client_metrics_ClientMetrics clientMetrics = gdt_client_metrics_ClientMetrics_init_default; + + pb_istream_t istream = pb_istream_from_buffer([data bytes], [data length]); + if (!pb_decode(&istream, gdt_client_metrics_ClientMetrics_fields, &clientMetrics)) { + if (outError != NULL) { + NSString *nanopbError = [NSString stringWithFormat:@"%s", PB_GET_ERROR(&istream)]; + *outError = [NSError errorWithDomain:NSStringFromClass(self) + code:-1 + userInfo:@{@"nanopb error" : nanopbError}]; + } + } + + return clientMetrics; +} + +@end diff --git a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.h b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.h new file mode 100644 index 00000000..6f8a72b1 --- /dev/null +++ b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.h @@ -0,0 +1,43 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +#include "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/client_metrics.nanopb.h" + +@class GDTCORMetrics; + +NS_ASSUME_NONNULL_BEGIN + +/// A test utility class that provides helpers when working with ``GDTCORMetrics`` instances. +@interface GDTCORMetricsTestHelpers : NSObject + +/// Creates and returns ``GDTCORMetrics`` instance with randomly generated dropped event counters +/// and storage metadata. ++ (GDTCORMetrics *)metricsWithGeneratedDroppedEventCounters; + +/// Creates and returns ``GDTCORMetrics`` instance with empty dropped event counters. ++ (GDTCORMetrics *)metricsWithEmptyDroppedEventCounters; + +/// Converts the given proto to a metrics object and then uses ``XCTAssertEqualObjects`` to assert +/// that the reconstructed metrics object equals the given metrics object. +/// @param metrics A given metrics object to compare. +/// @param metricsProto The given proto to reconstruct a metrics object with for comparison. ++ (void)assertMetrics:(GDTCORMetrics *)metrics + correspondToProto:(gdt_client_metrics_ClientMetrics)metricsProto; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.m b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.m new file mode 100644 index 00000000..619de2c5 --- /dev/null +++ b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.m @@ -0,0 +1,178 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/GDTCORMetricsTestHelpers.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h" + +#pragma mark - GDTCORLogSourceMetrics + Internal + +typedef NSDictionary GDTCORDroppedEventCounter; + +@interface GDTCORLogSourceMetrics (Internal) + +/// Initializer exposed for testing. +- (instancetype)initWithDroppedEventCounterByLogSource: + (NSDictionary *)droppedEventCounterByLogSource; + +@end + +#pragma mark - GDTCORMetrics + Internal + +@interface GDTCORMetrics (Internal) + +/// Initializer exposed for testing. +- (instancetype)initWithCollectionStartDate:(NSDate *)collectionStartDate + collectionEndDate:(NSDate *)collectionEndDate + logSourceMetrics:(GDTCORLogSourceMetrics *)logSourceMetrics + currentCacheSize:(GDTCORStorageSizeBytes)currentCacheSize + maxCacheSize:(GDTCORStorageSizeBytes)maxCacheSize + bundleID:(NSString *)bundleID; +@end + +#pragma mark - GDTCORMetricsTestHelpers + +@implementation GDTCORMetricsTestHelpers + ++ (GDTCORMetrics *)metricsWithGeneratedDroppedEventCounters { + // The below counter's structure was arbitrarily defined. + GDTCORLogSourceMetrics *logSourceMetrics = + [[GDTCORLogSourceMetrics alloc] initWithDroppedEventCounterByLogSource:@{ + @"log_src_1" : @{@(GDTCOREventDropReasonUnknown) : @(arc4random_uniform(5))}, + @"log_src_2" : @{ + @(GDTCOREventDropReasonStorageFull) : @(arc4random_uniform(100)), + @(GDTCOREventDropReasonServerError) : @(arc4random_uniform(100)), + }, + @"log_src_3" : @{ + @(GDTCOREventDropReasonUnknown) : @(arc4random_uniform(10)), + @(GDTCOREventDropReasonStorageFull) : @(arc4random_uniform(10)), + @(GDTCOREventDropReasonServerError) : @(arc4random_uniform(10)), + @(GDTCOREventDropReasonMessageTooOld) : @(arc4random_uniform(10)), + @(GDTCOREventDropReasonPayloadTooBig) : @(arc4random_uniform(10)), + @(GDTCOREventDropReasonInvalidPayload) : @(arc4random_uniform(10)), + @(GDTCOREventDropReasonMaxRetriesReached) : @(arc4random_uniform(10)), + }, + }]; + + return [[GDTCORMetrics alloc] initWithCollectionStartDate:[NSDate distantPast] + collectionEndDate:[NSDate distantFuture] + logSourceMetrics:logSourceMetrics + currentCacheSize:arc4random_uniform(20 * 1000 * 1000) + maxCacheSize:arc4random_uniform(20 * 1000 * 1000) + bundleID:@"com.test.bundle"]; +} + ++ (GDTCORMetrics *)metricsWithEmptyDroppedEventCounters { + return [[GDTCORMetrics alloc] initWithCollectionStartDate:[NSDate distantPast] + collectionEndDate:[NSDate distantFuture] + logSourceMetrics:[GDTCORLogSourceMetrics metrics] + currentCacheSize:arc4random_uniform(20 * 1000 * 1000) + maxCacheSize:arc4random_uniform(20 * 1000 * 1000) + bundleID:@"com.test.bundle"]; +} + ++ (void)assertMetrics:(GDTCORMetrics *)metrics + correspondToProto:(gdt_client_metrics_ClientMetrics)metricsProto { + GDTCORMetrics *metricsFromProto = GDTCCTConvertMetricsProtoToGDTCORMetrics(metricsProto); + XCTAssertEqualObjects(metrics, metricsFromProto); +} + +GDTCORMetrics *GDTCCTConvertMetricsProtoToGDTCORMetrics( + gdt_client_metrics_ClientMetrics metricsProto) { + // Reconstruct dates. + NSDate *collectionStartDate = + [[NSDate alloc] initWithTimeIntervalSince1970:metricsProto.window.start_ms / 1000]; + + NSDate *collectionEndDate = + [[NSDate alloc] initWithTimeIntervalSince1970:metricsProto.window.end_ms / 1000]; + + // Reconstruct log source metrics. + GDTCORLogSourceMetrics *logSourceMetrics = [GDTCORLogSourceMetrics metrics]; + + for (int logSourceIndex = 0; logSourceIndex < metricsProto.log_source_metrics_count; + logSourceIndex++) { + gdt_client_metrics_LogSourceMetrics logSourceMetricsProto = + metricsProto.log_source_metrics[logSourceIndex]; + + NSString *logSource = [[NSString alloc] initWithBytes:logSourceMetricsProto.log_source->bytes + length:logSourceMetricsProto.log_source->size + encoding:NSUTF8StringEncoding]; + + for (int logEventIndex = 0; logEventIndex < logSourceMetricsProto.log_event_dropped_count; + logEventIndex++) { + gdt_client_metrics_LogEventDropped logEventDropped = + logSourceMetricsProto.log_event_dropped[logEventIndex]; + + GDTCORLogSourceMetrics *logSourceDroppedEventCounter = + [[GDTCORLogSourceMetrics alloc] initWithDroppedEventCounterByLogSource:@{ + logSource : @{ + @(GDTCCTConvertProtoReasonToEventDropReason(logEventDropped.reason)) : + @(logEventDropped.events_dropped_count), + }, + }]; + + logSourceMetrics = [logSourceMetrics + logSourceMetricsByMergingWithLogSourceMetrics:logSourceDroppedEventCounter]; + } + } + + // Reconstruct storage metadata. + GDTCORStorageSizeBytes currentCacheSize = + metricsProto.global_metrics.storage_metrics.current_cache_size_bytes; + + GDTCORStorageSizeBytes maxCacheSize = + metricsProto.global_metrics.storage_metrics.max_cache_size_bytes; + + // Reconstruct bundle ID. + NSString *bundleID = [[NSString alloc] initWithBytes:metricsProto.app_namespace->bytes + length:metricsProto.app_namespace->size + encoding:NSUTF8StringEncoding]; + + return [[GDTCORMetrics alloc] initWithCollectionStartDate:collectionStartDate + collectionEndDate:collectionEndDate + logSourceMetrics:logSourceMetrics + currentCacheSize:currentCacheSize + maxCacheSize:maxCacheSize + bundleID:bundleID]; +} + +/// Returns the corresponding ``GDTCOREventDropReason`` for the given +/// ``gdt_client_metrics_LogEventDropped_Reason``. +/// @param protoReason The ``gdt_client_metrics_LogEventDropped_Reason`` to represent as +/// a ``GDTCOREventDropReason``. +GDTCOREventDropReason GDTCCTConvertProtoReasonToEventDropReason( + gdt_client_metrics_LogEventDropped_Reason protoReason) { + switch (protoReason) { + case gdt_client_metrics_LogEventDropped_Reason_REASON_UNKNOWN: + return GDTCOREventDropReasonUnknown; + case gdt_client_metrics_LogEventDropped_Reason_MESSAGE_TOO_OLD: + return GDTCOREventDropReasonMessageTooOld; + case gdt_client_metrics_LogEventDropped_Reason_CACHE_FULL: + return GDTCOREventDropReasonStorageFull; + case gdt_client_metrics_LogEventDropped_Reason_PAYLOAD_TOO_BIG: + return GDTCOREventDropReasonPayloadTooBig; + case gdt_client_metrics_LogEventDropped_Reason_MAX_RETRIES_REACHED: + return GDTCOREventDropReasonMaxRetriesReached; + // The below typo (`PAYLOD`) is currently checked in to g3. + case gdt_client_metrics_LogEventDropped_Reason_INVALID_PAYLOD: + return GDTCOREventDropReasonInvalidPayload; + case gdt_client_metrics_LogEventDropped_Reason_SERVER_ERROR: + return GDTCOREventDropReasonServerError; + } +} + +@end diff --git a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.h b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.h new file mode 100644 index 00000000..7dac4475 --- /dev/null +++ b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.h @@ -0,0 +1,25 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventDataObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (GDTCOREventDataObject) + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.m b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.m new file mode 100644 index 00000000..31d81fff --- /dev/null +++ b/GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.m @@ -0,0 +1,23 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCCTTests/Unit/Helpers/NSData+GDTCOREventDataObject.h" + +@implementation NSData (GDTCOREventDataObject) + +- (NSData *)transportBytes { + return [self copy]; +} + +@end diff --git a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTServiceExtension/NotificationService.swift b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTServiceExtension/NotificationService.swift index 7373fbd0..7d89d840 100644 --- a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTServiceExtension/NotificationService.swift +++ b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTServiceExtension/NotificationService.swift @@ -14,16 +14,15 @@ * limitations under the License. */ -import UserNotifications import FirebaseMessaging import GoogleDataTransport +import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? - var transport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.FLL)! + var transport = GDTCORTransport(mappingID: "1018", transformers: nil, target: GDTCORTarget.FLL)! override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) diff --git a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/InterfaceController.swift b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/InterfaceController.swift index e7c45d2a..62824a36 100644 --- a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/InterfaceController.swift +++ b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/InterfaceController.swift @@ -14,13 +14,12 @@ * limitations under the License. */ -import WatchKit import Foundation import GoogleDataTransport +import WatchKit class InterfaceController: WKInterfaceController { - var transport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.FLL)! + var transport = GDTCORTransport(mappingID: "1018", transformers: nil, target: GDTCORTarget.FLL)! override func awake(withContext context: Any?) { super.awake(withContext: context) diff --git a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/NotificationController.swift b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/NotificationController.swift index 1212a170..924cb24f 100644 --- a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/NotificationController.swift +++ b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSCompanionTestAppExtension/NotificationController.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import WatchKit import Foundation import UserNotifications +import WatchKit class NotificationController: WKUserNotificationInterfaceController { override init() { diff --git a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/InterfaceController.swift b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/InterfaceController.swift index 166423a3..faf6c044 100644 --- a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/InterfaceController.swift +++ b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/InterfaceController.swift @@ -14,13 +14,12 @@ * limitations under the License. */ -import WatchKit import Foundation import GoogleDataTransport +import WatchKit class InterfaceController: WKInterfaceController { - var transport: GDTCORTransport = GDTCORTransport(mappingID: "1018", transformers: nil, - target: GDTCORTarget.FLL)! + var transport = GDTCORTransport(mappingID: "1018", transformers: nil, target: GDTCORTarget.FLL)! override func awake(withContext context: Any?) { super.awake(withContext: context) diff --git a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/NotificationController.swift b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/NotificationController.swift index 03387acb..a57ecd16 100644 --- a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/NotificationController.swift +++ b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/NotificationController.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import WatchKit import Foundation import UserNotifications +import WatchKit class NotificationController: WKUserNotificationInterfaceController { override init() { diff --git a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/gdthelpers.swift b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/gdthelpers.swift index d1c4712f..779a1fd6 100644 --- a/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/gdthelpers.swift +++ b/GoogleDataTransport/GDTCCTWatchOSTestApp/GDTCCTWatchOSIndependentTestAppWatchKitExtension/gdthelpers.swift @@ -17,7 +17,7 @@ import GoogleDataTransport class FirelogTestMessageHolder: NSObject, GDTCOREventDataObject { - public var root: FirelogTestMessage = FirelogTestMessage() + public var root: FirelogTestMessage = .init() func transportBytes() -> Data { do { diff --git a/GoogleDataTransport/GDTCCTWatchOSTestApp/Podfile b/GoogleDataTransport/GDTCCTWatchOSTestApp/Podfile index 30f67f01..4f8fce37 100644 --- a/GoogleDataTransport/GDTCCTWatchOSTestApp/Podfile +++ b/GoogleDataTransport/GDTCCTWatchOSTestApp/Podfile @@ -1,5 +1,6 @@ use_frameworks! +source 'https://github.com/firebase/SpecsDev.git' source 'https://github.com/firebase/SpecsStaging.git' source 'https://cdn.cocoapods.org/' @@ -11,7 +12,7 @@ target 'GDTCCTWatchOSIndependentTestAppWatchKitExtension' do pod 'GoogleDataTransport', :path => '../../' pod 'FirebaseCore' pod 'FirebaseMessaging' - pod 'FirebaseStorage' + pod 'FirebaseStorage', '> 9.0' end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORAssert.m b/GoogleDataTransport/GDTCORLibrary/GDTCORAssert.m index 14462aea..4ab1aeb1 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORAssert.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORAssert.m @@ -24,8 +24,9 @@ GDTCORAssertionBlock GDTCORAssertionBlockToRunInstead(void) { if (assertionBlockSEL) { IMP assertionBlockIMP = [GDTCORAssertClass methodForSelector:assertionBlockSEL]; if (assertionBlockIMP) { - GDTCORAssertionBlock assertionBlock = ((GDTCORAssertionBlock(*)(id, SEL))assertionBlockIMP)( - GDTCORAssertClass, assertionBlockSEL); + GDTCORAssertionBlock assertionBlock = + ((GDTCORAssertionBlock (*)(id, SEL))assertionBlockIMP)(GDTCORAssertClass, + assertionBlockSEL); if (assertionBlock) { return assertionBlock; } diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORClock.m b/GoogleDataTransport/GDTCORLibrary/GDTCORClock.m index 85afebae..97f96fa9 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORClock.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORClock.m @@ -36,12 +36,10 @@ // difficult to unit test. /** Returns the kernel boottime property from sysctl. - * - * Inspired by https://stackoverflow.com/a/40497811 * * @return The KERN_BOOTTIME property from sysctl, in nanoseconds. */ -static int64_t KernelBootTimeInNanoseconds() { +static int64_t KernelBootTimeInNanoseconds(void) { // Caching the result is not possible because clock drift would not be accounted for. struct timeval boottime; int mib[2] = {CTL_KERN, KERN_BOOTTIME}; @@ -54,12 +52,10 @@ static int64_t KernelBootTimeInNanoseconds() { } /** Returns value of gettimeofday, in nanoseconds. - * - * Inspired by https://stackoverflow.com/a/40497811 * * @return The value of gettimeofday, in nanoseconds. */ -static int64_t UptimeInNanoseconds() { +static int64_t UptimeInNanoseconds(void) { int64_t before_now_nsec; int64_t after_now_nsec; struct timeval now; diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORDirectorySizeTracker.m b/GoogleDataTransport/GDTCORLibrary/GDTCORDirectorySizeTracker.m index 0bc85150..07dd958d 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORDirectorySizeTracker.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORDirectorySizeTracker.m @@ -15,6 +15,7 @@ */ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORDirectorySizeTracker.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h" @interface GDTCORDirectorySizeTracker () @@ -76,6 +77,9 @@ - (GDTCORStorageSizeBytes)calculateDirectoryContentSize { includingPropertiesForKeys:prefetchedProperties options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:^BOOL(NSURL *_Nonnull url, NSError *_Nonnull error) { + GDTCORLogWarning(GDTCORMCWFileReadError, + @"Error enumerating content size for URL %@: %@", url, + error); return YES; }]; diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCOREndpoints.m b/GoogleDataTransport/GDTCORLibrary/GDTCOREndpoints.m index eacc7b4e..f16db13d 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCOREndpoints.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCOREndpoints.m @@ -17,7 +17,7 @@ #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREndpoints.h" static NSString *const kINTServerURL = - @"https://dummyapiverylong-dummy.dummy.com/dummy/api/very/long"; + @"https://dummyapiverylong-dummy.google.com/dummy/api/very/long"; @implementation GDTCOREndpoints diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCOREvent.m b/GoogleDataTransport/GDTCORLibrary/GDTCOREvent.m index 5a8d3243..98db41ac 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCOREvent.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCOREvent.m @@ -21,6 +21,7 @@ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORClock.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h" #import "GoogleDataTransport/GDTCORLibrary/Private/GDTCOREvent_Private.h" @@ -31,7 +32,9 @@ + (NSString *)nextEventID { return [[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""]; } -- (nullable instancetype)initWithMappingID:(NSString *)mappingID target:(GDTCORTarget)target { +- (nullable instancetype)initWithMappingID:(NSString *)mappingID + productData:(nullable GDTCORProductData *)productData + target:(GDTCORTarget)target { GDTCORAssert(mappingID.length > 0, @"Please give a valid mapping ID"); GDTCORAssert(target > 0, @"A target cannot be negative or 0"); if (mappingID.length == 0 || target <= 0) { @@ -41,6 +44,7 @@ - (nullable instancetype)initWithMappingID:(NSString *)mappingID target:(GDTCORT if (self) { _eventID = [GDTCOREvent nextEventID]; _mappingID = mappingID; + _productData = productData; _target = target; _qosTier = GDTCOREventQosDefault; _expirationDate = [NSDate dateWithTimeIntervalSinceNow:604800]; // 7 days. @@ -52,8 +56,14 @@ - (nullable instancetype)initWithMappingID:(NSString *)mappingID target:(GDTCORT return self; } +- (nullable instancetype)initWithMappingID:(NSString *)mappingID target:(GDTCORTarget)target { + return [self initWithMappingID:mappingID productData:nil target:target]; +} + - (instancetype)copy { - GDTCOREvent *copy = [[GDTCOREvent alloc] initWithMappingID:_mappingID target:_target]; + GDTCOREvent *copy = [[GDTCOREvent alloc] initWithMappingID:_mappingID + productData:_productData + target:_target]; copy->_eventID = _eventID; copy.dataObject = _dataObject; copy.qosTier = _qosTier; @@ -67,10 +77,12 @@ - (NSUInteger)hash { // This loses some precision, but it's probably fine. NSUInteger eventIDHash = [_eventID hash]; NSUInteger mappingIDHash = [_mappingID hash]; + NSUInteger productDataHash = [_productData hash]; NSUInteger timeHash = [_clockSnapshot hash]; NSInteger serializedBytesHash = [_serializedDataObjectBytes hash]; - return eventIDHash ^ mappingIDHash ^ _target ^ _qosTier ^ timeHash ^ serializedBytesHash; + return eventIDHash ^ mappingIDHash ^ productDataHash ^ _target ^ _qosTier ^ timeHash ^ + serializedBytesHash; } - (BOOL)isEqual:(id)object { @@ -115,6 +127,9 @@ - (void)setDataObject:(id)dataObject { /** NSCoding key for customData property. */ static NSString *kCustomDataKey = @"GDTCOREventCustomDataKey"; +/** NSCoding key for productData property. */ +static NSString *kProductDataKey = @"GDTCOREventProductDataKey"; + + (BOOL)supportsSecureCoding { return YES; } @@ -123,6 +138,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { self = [self init]; if (self) { _mappingID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kMappingIDKey]; + _productData = [aDecoder decodeObjectOfClass:[GDTCORProductData class] forKey:kProductDataKey]; _target = [aDecoder decodeIntegerForKey:kTargetKey]; _eventID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kEventIDKey] ?: [GDTCOREvent nextEventID]; @@ -142,6 +158,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_eventID forKey:kEventIDKey]; [aCoder encodeObject:_mappingID forKey:kMappingIDKey]; + [aCoder encodeObject:_productData forKey:kProductDataKey]; [aCoder encodeInteger:_target forKey:kTargetKey]; [aCoder encodeInteger:_qosTier forKey:kQoSTierKey]; [aCoder encodeObject:_clockSnapshot forKey:kClockSnapshotKey]; diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage+Promises.m b/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage+Promises.m index e27e2af6..4ff937c6 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage+Promises.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage+Promises.m @@ -22,6 +22,10 @@ #import "FBLPromises.h" #endif +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h" #import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadBatch.h" @implementation GDTCORFlatFileStorage (Promises) @@ -58,7 +62,11 @@ @implementation GDTCORFlatFileStorage (Promises) deleteEvents:(BOOL)deleteEvents { return [self batchIDsForTarget:target].thenOn(self.storageQueue, ^id(NSSet *batchIDs) { - return [self removeBatchesWithIDs:batchIDs deleteEvents:NO]; + if (batchIDs.count == 0) { + return [FBLPromise resolvedWith:[NSNull null]]; + } else { + return [self removeBatchesWithIDs:batchIDs deleteEvents:NO]; + } }); } @@ -90,6 +98,55 @@ @implementation GDTCORFlatFileStorage (Promises) }]; } +- (FBLPromise *)fetchAndUpdateMetricsWithHandler: + (GDTCORMetricsMetadata * (^)(GDTCORMetricsMetadata *_Nullable fetchedMetadata, + NSError *_Nullable fetchError))handler { + return FBLPromise.doOn(self.storageQueue, ^id { + // Fetch the stored metrics metadata. + NSError *decodeError; + NSString *metricsMetadataPath = + [[[self class] libraryDataStoragePath] stringByAppendingPathComponent:@"metrics_metadata"]; + GDTCORMetricsMetadata *decodedMetadata = (GDTCORMetricsMetadata *)GDTCORDecodeArchiveAtPath( + GDTCORMetricsMetadata.class, metricsMetadataPath, &decodeError); + + // Update the metadata using the retrieved metadata. + GDTCORMetricsMetadata *updatedMetadata = handler(decodedMetadata, decodeError); + if (updatedMetadata == nil) { + // `nil` metadata is not expected and will be a no-op. + return nil; + } + + if (![updatedMetadata isEqual:decodedMetadata]) { + // The metadata was updated so it needs to be saved. + // - Encode the updated metadata. + NSError *encodeError; + NSData *encodedMetadata = GDTCOREncodeArchive(updatedMetadata, nil, &encodeError); + if (encodeError) { + return encodeError; + } + + // - Write the encoded metadata to disk. + NSError *writeError; + BOOL writeResult = GDTCORWriteDataToFile(encodedMetadata, metricsMetadataPath, &writeError); + if (writeResult == NO || writeError) { + return writeError; + } + } + + return nil; + }); +} + +- (FBLPromise *)fetchStorageMetadata { + return FBLPromise.asyncOn(self.storageQueue, ^(FBLPromiseFulfillBlock _Nonnull fulfill, + FBLPromiseRejectBlock _Nonnull reject) { + [self storageSizeWithCallback:^(GDTCORStorageSizeBytes storageSize) { + fulfill([GDTCORStorageMetadata metadataWithCurrentCacheSize:storageSize + maxCacheSize:kGDTCORFlatFileStorageSizeLimit]); + }]; + }); +} + // TODO: Move to a separate class/extension when needed in more places. - (NSError *)genericRejectedPromiseErrorWithReason:(NSString *)reason { return [NSError errorWithDomain:@"GDTCORFlatFileStorage" @@ -98,3 +155,7 @@ - (NSError *)genericRejectedPromiseErrorWithReason:(NSString *)reason { } @end + +/// Stub used to force the linker to include the categories in this file. +void GDTCORInclude_GDTCORLogSourceMetrics_Internal_Category(void) { +} diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage.m b/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage.m index 42cca001..fe36a198 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage.m @@ -65,6 +65,7 @@ @interface GDTCORFlatFileStorage () @implementation GDTCORFlatFileStorage @synthesize sizeTracker = _sizeTracker; +@synthesize delegate = _delegate; + (void)load { #if !NDEBUG @@ -157,6 +158,11 @@ - (void)storeEvent:(GDTCOREvent *)event userInfo:@{ NSLocalizedFailureReasonErrorKey : @"Storage size limit has been reached." }]; + if (self.delegate != nil) { + GDTCORLogDebug(@"Delegate notified that event with mapping ID %@ was dropped.", + event.mappingID); + [self.delegate storage:self didDropEvent:event]; + } completion(NO, error); return; } @@ -200,7 +206,7 @@ - (void)batchWithEventSelector:(nonnull GDTCORStorageEventSelector *)eventSelect for (NSString *eventPath in paths) { NSError *error; GDTCOREvent *event = - (GDTCOREvent *)GDTCORDecodeArchive([GDTCOREvent class], eventPath, nil, &error); + (GDTCOREvent *)GDTCORDecodeArchiveAtPath([GDTCOREvent class], eventPath, &error); if (event == nil || error) { GDTCORLogDebug(@"Error deserializing event: %@", error); [[NSFileManager defaultManager] removeItemAtPath:eventPath error:nil]; @@ -296,7 +302,7 @@ - (void)batchIDsForTarget:(GDTCORTarget)target NSDictionary *components = [self batchComponentsFromFilename:path]; NSNumber *targetNumber = components[kGDTCORBatchComponentsTargetKey]; NSNumber *batchID = components[kGDTCORBatchComponentsBatchIDKey]; - if (targetNumber.intValue == target) { + if (batchID != nil && targetNumber.intValue == target) { [batchIDs addObject:batchID]; } } @@ -421,6 +427,7 @@ - (void)checkForExpirations { } // Find expired events and remove them from the storage. + NSMutableSet *expiredEvents = [NSMutableSet set]; NSString *eventDataPath = [GDTCORFlatFileStorage eventDataStoragePath]; NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:eventDataPath]; NSString *path; @@ -440,17 +447,38 @@ - (void)checkForExpirations { NSDate *expirationDate = eventComponents[kGDTCOREventComponentsExpirationKey]; if (expirationDate != nil && expirationDate.timeIntervalSince1970 < now) { NSString *pathToDelete = [eventDataPath stringByAppendingPathComponent:path]; - NSError *error; - [fileManager removeItemAtPath:pathToDelete error:&error]; - if (error != nil) { - GDTCORLogDebug(@"There was an error deleting an expired item: %@", error); + + // Decode the expired event from the path to be deleted. + NSError *decodeError; + GDTCOREvent *event = (GDTCOREvent *)GDTCORDecodeArchiveAtPath([GDTCOREvent class], + pathToDelete, &decodeError); + if (event == nil || decodeError) { + GDTCORLogDebug(@"Error deserializing event while checking for expired events: %@", + decodeError); + event = nil; + } + + // Remove the path to be deleted, adding the decoded event to the + // event set if the removal was successful. + NSError *removeError; + [fileManager removeItemAtPath:pathToDelete error:&removeError]; + if (removeError != nil) { + GDTCORLogDebug(@"There was an error deleting an expired item: %@", removeError); } else { GDTCORLogDebug(@"Item deleted because it expired: %@", pathToDelete); + if (event) { + [expiredEvents addObject:event]; + } } } } } + if (self.delegate != nil && [expiredEvents count] > 0) { + GDTCORLogDebug(@"Delegate notified that %@ events were dropped.", @(expiredEvents.count)); + [self.delegate storage:self didRemoveExpiredEvents:[expiredEvents copy]]; + } + [self.sizeTracker resetCachedSize]; }); } @@ -474,7 +502,7 @@ - (void)storageSizeWithCallback:(void (^)(uint64_t storageSize))onComplete { * cases when the app is terminated while uploading a batch. */ - (nullable NSArray *)batchDirPathsForBatchID:(NSNumber *)batchID - error:(NSError **)outError { + error:(NSError **_Nonnull)outError { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; NSArray *batches = @@ -503,7 +531,7 @@ - (void)storageSizeWithCallback:(void (^)(uint64_t storageSize))onComplete { /** Makes a copy of the contents of a directory to a directory at the specified path.*/ - (BOOL)moveContentsOfDirectoryAtPath:(NSString *)sourcePath to:(NSString *)destinationPath - error:(NSError **)outError { + error:(NSError **_Nonnull)outError { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; @@ -566,14 +594,19 @@ - (void)syncThreadUnsafeRemoveBatchWithID:(nonnull NSNumber *)batchID } else { NSString *batchDirName = [batchDirPath lastPathComponent]; NSDictionary *components = [self batchComponentsFromFilename:batchDirName]; - NSNumber *target = components[kGDTCORBatchComponentsTargetKey]; - NSString *destinationPath = [[GDTCORFlatFileStorage eventDataStoragePath] - stringByAppendingPathComponent:target.stringValue]; + NSString *targetValue = [components[kGDTCORBatchComponentsTargetKey] stringValue]; + NSString *destinationPath; + if (targetValue) { + destinationPath = [[GDTCORFlatFileStorage eventDataStoragePath] + stringByAppendingPathComponent:targetValue]; + } // `- [NSFileManager moveItemAtPath:toPath:error:]` method fails if an item by the // destination path already exists (which usually is the case for the current method). Move // the events one by one instead. - if ([self moveContentsOfDirectoryAtPath:batchDirPath to:destinationPath error:&error]) { + if (destinationPath && [self moveContentsOfDirectoryAtPath:batchDirPath + to:destinationPath + error:&error]) { GDTCORLogDebug(@"Batched events at path: %@ moved back to the storage: %@", batchDirPath, destinationPath); } else { @@ -581,7 +614,7 @@ - (void)syncThreadUnsafeRemoveBatchWithID:(nonnull NSNumber *)batchID } // Even if not all events where moved back to the storage, there is not much can be done at - // this point, so cleanup batch directory now to avoid clattering. + // this point, so cleanup batch directory now to avoid cluttering. removeBatchDir(batchDirPath); } } @@ -811,11 +844,6 @@ - (void)appWillBackground:(GDTCORApplication *)app { }); } -- (void)appWillTerminate:(GDTCORApplication *)application { - dispatch_sync(_storageQueue, ^{ - }); -} - @end NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORLifecycle.m b/GoogleDataTransport/GDTCORLibrary/GDTCORLifecycle.m index 89da75dd..7e534569 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORLifecycle.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORLifecycle.m @@ -47,18 +47,17 @@ - (instancetype)init { if (self) { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self - selector:@selector(applicationDidEnterBackground:) + selector:@selector(applicationDidEnterBackgroundNotification:) name:kGDTCORApplicationDidEnterBackgroundNotification object:nil]; [notificationCenter addObserver:self - selector:@selector(applicationWillEnterForeground:) + selector:@selector(applicationWillEnterForegroundNotification:) name:kGDTCORApplicationWillEnterForegroundNotification object:nil]; - NSString *name = kGDTCORApplicationWillTerminateNotification; [notificationCenter addObserver:self - selector:@selector(applicationWillTerminate:) - name:name + selector:@selector(applicationWillTerminateNotification:) + name:kGDTCORApplicationWillTerminateNotification object:nil]; } return self; @@ -68,7 +67,7 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)applicationDidEnterBackground:(NSNotification *)notification { +- (void)applicationDidEnterBackgroundNotification:(NSNotification *)notification { GDTCORApplication *application = [GDTCORApplication sharedApplication]; if ([[GDTCORTransformer sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { GDTCORLogDebug(@"%@", @"Signaling GDTCORTransformer that the app is backgrounding."); @@ -84,7 +83,7 @@ - (void)applicationDidEnterBackground:(NSNotification *)notification { } } -- (void)applicationWillEnterForeground:(NSNotification *)notification { +- (void)applicationWillEnterForegroundNotification:(NSNotification *)notification { GDTCORApplication *application = [GDTCORApplication sharedApplication]; if ([[GDTCORTransformer sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { GDTCORLogDebug(@"%@", @"Signaling GDTCORTransformer that the app is foregrounding."); @@ -100,7 +99,7 @@ - (void)applicationWillEnterForeground:(NSNotification *)notification { } } -- (void)applicationWillTerminate:(NSNotification *)notification { +- (void)applicationWillTerminateNotification:(NSNotification *)notification { GDTCORApplication *application = [GDTCORApplication sharedApplication]; if ([[GDTCORTransformer sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { GDTCORLogDebug(@"%@", @"Signaling GDTCORTransformer that the app is terminating."); diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORLogSourceMetrics.m b/GoogleDataTransport/GDTCORLibrary/GDTCORLogSourceMetrics.m new file mode 100644 index 00000000..9da40dbb --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORLogSourceMetrics.m @@ -0,0 +1,184 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" + +static NSString *const kDroppedEventCounterByLogSource = @"droppedEventCounterByLogSource"; + +typedef NSDictionary GDTCORDroppedEventCounter; + +@interface GDTCORLogSourceMetrics () + +/// A dictionary of log sources that map to counters that reflect the number of events dropped for a +/// given set of reasons (``GDTCOREventDropReason``). +@property(nonatomic, readonly) + NSDictionary *droppedEventCounterByLogSource; + +@end + +@implementation GDTCORLogSourceMetrics + ++ (instancetype)metrics { + return [[self alloc] initWithDroppedEventCounterByLogSource:@{}]; +} + ++ (instancetype)metricsWithEvents:(NSArray *)events + droppedForReason:(GDTCOREventDropReason)reason { + NSMutableDictionary *eventCounterByLogSource = + [NSMutableDictionary dictionary]; + + for (GDTCOREvent *event in [events copy]) { + // Dropped events with a `nil` or empty mapping ID (log source) are not recorded. + if (event.mappingID.length == 0) { + continue; + } + + // Get the dropped event counter for the event's mapping ID (log source). + // If the dropped event counter for this event's mapping ID is `nil`, + // an empty mutable counter is returned. + NSMutableDictionary *eventCounter = [NSMutableDictionary + dictionaryWithDictionary:eventCounterByLogSource[event.mappingID] ?: @{}]; + + // Increment the log source metrics for the given reason. + NSInteger currentEventCountForReason = [eventCounter[@(reason)] integerValue]; + NSInteger updatedEventCountForReason = currentEventCountForReason + 1; + + eventCounter[@(reason)] = @(updatedEventCountForReason); + + // Update the mapping ID's (log source's) event counter with an immutable + // copy of the updated counter. + eventCounterByLogSource[event.mappingID] = [eventCounter copy]; + } + + return [[self alloc] initWithDroppedEventCounterByLogSource:[eventCounterByLogSource copy]]; +} + +- (instancetype)initWithDroppedEventCounterByLogSource: + (NSDictionary *)droppedEventCounterByLogSource { + self = [super init]; + if (self) { + _droppedEventCounterByLogSource = [droppedEventCounterByLogSource copy]; + } + return self; +} + +- (GDTCORLogSourceMetrics *)logSourceMetricsByMergingWithLogSourceMetrics: + (GDTCORLogSourceMetrics *)metrics { + // Create new log source metrics by merging the current metrics with the given metrics. + NSDictionary *mergedEventCounterByLogSource = [[self + class] dictionaryByMergingDictionary:self.droppedEventCounterByLogSource + withOtherDictionary:metrics.droppedEventCounterByLogSource + uniquingKeysWithBlock:^NSDictionary *(NSDictionary *eventCounter1, + NSDictionary *eventCounter2) { + return [[self class] + dictionaryByMergingDictionary:eventCounter1 + withOtherDictionary:eventCounter2 + uniquingKeysWithBlock:^NSNumber *(NSNumber *eventCount1, + NSNumber *eventCount2) { + return @(eventCount1.integerValue + eventCount2.integerValue); + }]; + }]; + + return + [[[self class] alloc] initWithDroppedEventCounterByLogSource:mergedEventCounterByLogSource]; +} + +/// Creates a new dictionary by merging together two given dictionaries. +/// @param dictionary A dictionary for merging. +/// @param otherDictionary Another dictionary for merging. +/// @param block A block that is called with the values for any duplicate keys that are encountered. +/// The block returns the desired value for the merged dictionary. ++ (NSDictionary *)dictionaryByMergingDictionary:(NSDictionary *)dictionary + withOtherDictionary:(NSDictionary *)otherDictionary + uniquingKeysWithBlock:(id (^)(id value1, id value2))block { + NSMutableDictionary *mergedDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionary]; + + [otherDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if (mergedDictionary[key] != nil) { + // The key exists in both given dictionaries so combine the corresponding values with the + // given block. + id newValue = block(mergedDictionary[key], obj); + mergedDictionary[key] = newValue; + } else { + mergedDictionary[key] = obj; + } + }]; + + return [mergedDictionary copy]; +} + +#pragma mark - Equality + +- (BOOL)isEqualToLogSourceMetrics:(GDTCORLogSourceMetrics *)otherMetrics { + return [_droppedEventCounterByLogSource + isEqualToDictionary:otherMetrics.droppedEventCounterByLogSource]; +} + +- (BOOL)isEqual:(nullable id)object { + if (object == nil) { + return NO; + } + + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[self class]]) { + return NO; + } + + return [self isEqualToLogSourceMetrics:(GDTCORLogSourceMetrics *)object]; +} + +- (NSUInteger)hash { + return [_droppedEventCounterByLogSource hash]; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder { + NSDictionary *droppedEventCounterByLogSource = + [coder decodeObjectOfClasses: + [NSSet setWithArray:@[ NSDictionary.class, NSString.class, NSNumber.class ]] + forKey:kDroppedEventCounterByLogSource]; + + if (!droppedEventCounterByLogSource || + ![droppedEventCounterByLogSource isKindOfClass:[NSDictionary class]]) { + // If any of the fields are corrupted, the initializer should fail. + return nil; + } + + return [self initWithDroppedEventCounterByLogSource:droppedEventCounterByLogSource]; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)coder { + [coder encodeObject:self.droppedEventCounterByLogSource forKey:kDroppedEventCounterByLogSource]; +} + +#pragma mark - Description + +- (NSString *)description { + return [NSString + stringWithFormat:@"%@ %@", [super description], self.droppedEventCounterByLogSource]; +} + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORMetrics.m b/GoogleDataTransport/GDTCORLibrary/GDTCORMetrics.m new file mode 100644 index 00000000..ffbf0e11 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORMetrics.m @@ -0,0 +1,100 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h" + +@implementation GDTCORMetrics + +- (instancetype)initWithCollectionStartDate:(NSDate *)collectionStartDate + collectionEndDate:(NSDate *)collectionEndDate + logSourceMetrics:(GDTCORLogSourceMetrics *)logSourceMetrics + currentCacheSize:(GDTCORStorageSizeBytes)currentCacheSize + maxCacheSize:(GDTCORStorageSizeBytes)maxCacheSize + bundleID:(NSString *)bundleID { + self = [super init]; + if (self) { + _collectionStartDate = [collectionStartDate copy]; + _collectionEndDate = [collectionEndDate copy]; + _logSourceMetrics = logSourceMetrics; + _currentCacheSize = currentCacheSize; + _maxCacheSize = maxCacheSize; + _bundleID = [bundleID copy]; + } + return self; +} + ++ (instancetype)metricsWithMetricsMetadata:(GDTCORMetricsMetadata *)metricsMetadata + storageMetadata:(GDTCORStorageMetadata *)storageMetadata { + // The window of collection ends at the time of creating the metrics object. + NSDate *collectionEndDate = [NSDate date]; + // The main bundle ID is associated with the created metrics. + NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier] ?: @""; + + return [[GDTCORMetrics alloc] initWithCollectionStartDate:metricsMetadata.collectionStartDate + collectionEndDate:collectionEndDate + logSourceMetrics:metricsMetadata.logSourceMetrics + currentCacheSize:storageMetadata.currentCacheSize + maxCacheSize:storageMetadata.maxCacheSize + bundleID:bundleID]; +} + +#pragma mark - Equality + +- (BOOL)isEqualToMetrics:(GDTCORMetrics *)otherMetrics { + return [self.collectionStartDate isEqualToDate:otherMetrics.collectionStartDate] && + [self.collectionEndDate isEqualToDate:otherMetrics.collectionEndDate] && + [self.logSourceMetrics isEqualToLogSourceMetrics:otherMetrics.logSourceMetrics] && + [self.bundleID isEqualToString:otherMetrics.bundleID] && + self.currentCacheSize == otherMetrics.currentCacheSize && + self.maxCacheSize == otherMetrics.maxCacheSize; +} + +- (BOOL)isEqual:(nullable id)object { + if (object == nil) { + return NO; + } + + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[self class]]) { + return NO; + } + + return [self isEqualToMetrics:(GDTCORMetrics *)object]; +} + +- (NSUInteger)hash { + return [self.collectionStartDate hash] ^ [self.collectionEndDate hash] ^ + [self.logSourceMetrics hash] ^ [self.bundleID hash] ^ [@(self.currentCacheSize) hash] ^ + [@(self.maxCacheSize) hash]; +} + +#pragma mark - Description + +- (NSString *)description { + return [NSString + stringWithFormat: + @"%@ {\n\tcollectionStartDate: %@,\n\tcollectionEndDate: %@,\n\tcurrentCacheSize: " + @"%llu,\n\tmaxCacheSize: %llu,\n\tbundleID: %@,\n\tlogSourceMetrics: %@}\n", + [super description], self.collectionStartDate, self.collectionEndDate, + self.currentCacheSize, self.maxCacheSize, self.bundleID, self.logSourceMetrics]; +} + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORMetricsController.m b/GoogleDataTransport/GDTCORLibrary/GDTCORMetricsController.m new file mode 100644 index 00000000..2978da76 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORMetricsController.m @@ -0,0 +1,201 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsController.h" + +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h" +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORFlatFileStorage+Promises.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h" + +@interface GDTCORMetricsController () +/// The underlying storage object where metrics are stored. +@property(nonatomic) id storage; + +@end + +@implementation GDTCORMetricsController + ++ (void)load { +#if GDT_TEST + [[GDTCORRegistrar sharedInstance] registerMetricsController:[self sharedInstance] + target:kGDTCORTargetTest]; +#endif // GDT_TEST + // Only the Firelog backend supports metrics collection. + [[GDTCORRegistrar sharedInstance] registerMetricsController:[self sharedInstance] + target:kGDTCORTargetCSH]; + [[GDTCORRegistrar sharedInstance] registerMetricsController:[self sharedInstance] + target:kGDTCORTargetFLL]; +} + ++ (instancetype)sharedInstance { + static id sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] initWithStorage:[GDTCORFlatFileStorage sharedInstance]]; + }); + return sharedInstance; +} + +- (instancetype)initWithStorage:(id)storage { + self = [super init]; + if (self) { + _storage = storage; + } + return self; +} + +- (nonnull FBLPromise *)logEventsDroppedForReason:(GDTCOREventDropReason)reason + events:(nonnull NSSet *)events { + // No-op if there are no events to log. + if ([events count] == 0) { + return [FBLPromise resolvedWith:nil]; + } + + __auto_type handler = ^GDTCORMetricsMetadata *(GDTCORMetricsMetadata *_Nullable metricsMetadata, + NSError *_Nullable fetchError) { + GDTCORLogSourceMetrics *logSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:[events allObjects] droppedForReason:reason]; + + if (metricsMetadata) { + GDTCORLogSourceMetrics *updatedLogSourceMetrics = [metricsMetadata.logSourceMetrics + logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics]; + + return [GDTCORMetricsMetadata + metadataWithCollectionStartDate:[metricsMetadata collectionStartDate] + logSourceMetrics:updatedLogSourceMetrics]; + } else { + // There was an error (e.g. empty storage); `metricsMetadata` is nil. + GDTCORLogDebug(@"Error fetching metrics metadata: %@", fetchError); + return [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate date] + logSourceMetrics:logSourceMetrics]; + } + }; + + return [_storage fetchAndUpdateMetricsWithHandler:handler]; +} + +- (nonnull FBLPromise *)getAndResetMetrics { + __block GDTCORMetricsMetadata *_Nullable snapshottedMetricsMetadata = nil; + + __auto_type handler = ^GDTCORMetricsMetadata *(GDTCORMetricsMetadata *_Nullable metricsMetadata, + NSError *_Nullable fetchError) { + if (metricsMetadata) { + snapshottedMetricsMetadata = metricsMetadata; + } else { + GDTCORLogDebug(@"Error fetching metrics metadata: %@", fetchError); + } + return [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate date] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + }; + + return [_storage fetchAndUpdateMetricsWithHandler:handler] + .validate(^BOOL(NSNull *__unused _) { + // Break and reject the promise chain when storage contains no metrics + // metadata. + return snapshottedMetricsMetadata != nil; + }) + .then(^FBLPromise *(NSNull *__unused _) { + // Fetch and return storage metadata (needed for metrics). + return [self.storage fetchStorageMetadata]; + }) + .then(^GDTCORMetrics *(GDTCORStorageMetadata *storageMetadata) { + // Use the fetched metrics & storage metadata to create and return a + // complete metrics object. + return [GDTCORMetrics metricsWithMetricsMetadata:snapshottedMetricsMetadata + storageMetadata:storageMetadata]; + }); +} + +- (nonnull FBLPromise *)offerMetrics:(nonnull GDTCORMetrics *)metrics { + // No-op if there are no metrics to offer. + if (metrics == nil) { + return [FBLPromise resolvedWith:nil]; + } + + __auto_type handler = ^GDTCORMetricsMetadata *(GDTCORMetricsMetadata *_Nullable metricsMetadata, + NSError *_Nullable fetchError) { + if (metricsMetadata) { + if (metrics.collectionStartDate.timeIntervalSince1970 <= + metricsMetadata.collectionStartDate.timeIntervalSince1970) { + // If the metrics to append are older than the metrics represented by + // the currently stored metrics, then return a new metadata object that + // incorporates the data from the given metrics. + return [GDTCORMetricsMetadata + metadataWithCollectionStartDate:[metrics collectionStartDate] + logSourceMetrics:[metricsMetadata.logSourceMetrics + logSourceMetricsByMergingWithLogSourceMetrics: + metrics.logSourceMetrics]]; + } else { + // This catches an edge case where the given metrics to append are + // newer than metrics represented by the currently stored metrics + // metadata. In this case, return the existing metadata object as the + // given metrics are assumed to already be accounted for by the + // currently stored metadata. + return metricsMetadata; + } + } else { + // There was an error (e.g. empty storage); `metricsMetadata` is nil. + GDTCORLogDebug(@"Error fetching metrics metadata: %@", fetchError); + + NSDate *now = [NSDate date]; + if (metrics.collectionStartDate.timeIntervalSince1970 <= now.timeIntervalSince1970) { + // The given metrics are were recorded up until now. They wouldn't + // be offered if they were successfully uploaded so their + // corresponding metadata can be safely placed back in storage. + return [GDTCORMetricsMetadata metadataWithCollectionStartDate:metrics.collectionStartDate + logSourceMetrics:metrics.logSourceMetrics]; + } else { + // This catches an edge case where the given metrics are from the + // future. If this occurs, ignore them and store an empty metadata + // object intended to track metrics metadata from this time forward. + return [GDTCORMetricsMetadata + metadataWithCollectionStartDate:[NSDate date] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + } + } + }; + + return [_storage fetchAndUpdateMetricsWithHandler:handler]; +} + +#pragma mark - GDTCORStorageDelegate + +- (void)storage:(id)storage + didRemoveExpiredEvents:(nonnull NSSet *)events { + [self logEventsDroppedForReason:GDTCOREventDropReasonMessageTooOld events:events]; +} + +- (void)storage:(nonnull id)storage + didDropEvent:(nonnull GDTCOREvent *)event { + [self logEventsDroppedForReason:GDTCOREventDropReasonStorageFull + events:[NSSet setWithObject:event]]; +} + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORMetricsMetadata.m b/GoogleDataTransport/GDTCORLibrary/GDTCORMetricsMetadata.m new file mode 100644 index 00000000..624c8c5c --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORMetricsMetadata.m @@ -0,0 +1,96 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" + +static NSString *const kCollectionStartDate = @"collectionStartDate"; +static NSString *const kLogSourceMetrics = @"logSourceMetrics"; + +@implementation GDTCORMetricsMetadata + ++ (instancetype)metadataWithCollectionStartDate:(NSDate *)collectedSinceDate + logSourceMetrics:(GDTCORLogSourceMetrics *)logSourceMetrics { + return [[self alloc] initWithCollectionStartDate:collectedSinceDate + logSourceMetrics:logSourceMetrics]; +} + +- (instancetype)initWithCollectionStartDate:(NSDate *)collectionStartDate + logSourceMetrics:(GDTCORLogSourceMetrics *)logSourceMetrics { + self = [super init]; + if (self) { + _collectionStartDate = [collectionStartDate copy]; + _logSourceMetrics = logSourceMetrics; + } + return self; +} + +#pragma mark - Equality + +- (BOOL)isEqualToMetricsMetadata:(GDTCORMetricsMetadata *)otherMetricsMetadata { + return [self.collectionStartDate isEqualToDate:otherMetricsMetadata.collectionStartDate] && + [self.logSourceMetrics isEqualToLogSourceMetrics:otherMetricsMetadata.logSourceMetrics]; +} + +- (BOOL)isEqual:(nullable id)object { + if (object == nil) { + return NO; + } + + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[self class]]) { + return NO; + } + + return [self isEqualToMetricsMetadata:(GDTCORMetricsMetadata *)object]; +} + +- (NSUInteger)hash { + return [self.collectionStartDate hash] ^ [self.logSourceMetrics hash]; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder { + NSDate *collectionStartDate = [coder decodeObjectOfClass:[NSDate class] + forKey:kCollectionStartDate]; + GDTCORLogSourceMetrics *logSourceMetrics = + [coder decodeObjectOfClass:[GDTCORLogSourceMetrics class] forKey:kLogSourceMetrics]; + + if (!collectionStartDate || !logSourceMetrics || + ![collectionStartDate isKindOfClass:[NSDate class]] || + ![logSourceMetrics isKindOfClass:[GDTCORLogSourceMetrics class]]) { + // If any of the fields are corrupted, the initializer should fail. + return nil; + } + + return [self initWithCollectionStartDate:collectionStartDate logSourceMetrics:logSourceMetrics]; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)coder { + [coder encodeObject:self.collectionStartDate forKey:kCollectionStartDate]; + [coder encodeObject:self.logSourceMetrics forKey:kLogSourceMetrics]; +} + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORPlatform.m b/GoogleDataTransport/GDTCORLibrary/GDTCORPlatform.m index a6207ec2..0729bde9 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORPlatform.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORPlatform.m @@ -84,7 +84,7 @@ BOOL GDTCORReachabilityFlagsContainWWAN(GDTCORNetworkReachabilityFlags flags) { #endif // TARGET_OS_IOS } -GDTCORNetworkType GDTCORNetworkTypeMessage() { +GDTCORNetworkType GDTCORNetworkTypeMessage(void) { #if !TARGET_OS_WATCH SCNetworkReachabilityFlags reachabilityFlags = [GDTCORReachability currentFlags]; if ((reachabilityFlags & kSCNetworkReachabilityFlagsReachable) == @@ -99,8 +99,10 @@ GDTCORNetworkType GDTCORNetworkTypeMessage() { return GDTCORNetworkTypeUNKNOWN; } -GDTCORNetworkMobileSubtype GDTCORNetworkMobileSubTypeMessage() { -#if TARGET_OS_IOS +GDTCORNetworkMobileSubtype GDTCORNetworkMobileSubTypeMessage(void) { +// TODO(Xcode 15): When Xcode 15 is the minimum supported Xcode version, +// it will be unnecessary to check if `TARGET_OS_VISION` is defined. +#if TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION) static NSDictionary *CTRadioAccessTechnologyToNetworkSubTypeMessage; static CTTelephonyNetworkInfo *networkInfo; static dispatch_once_t onceToken; @@ -127,19 +129,13 @@ GDTCORNetworkMobileSubtype GDTCORNetworkMobileSubTypeMessage() { if (networkCurrentRadioAccessTechnologyDict.count) { networkCurrentRadioAccessTechnology = networkCurrentRadioAccessTechnologyDict.allValues[0]; } -#else // TARGET_OS_MACCATALYST - if (@available(iOS 12.0, *)) { - NSDictionary *networkCurrentRadioAccessTechnologyDict = - networkInfo.serviceCurrentRadioAccessTechnology; - if (networkCurrentRadioAccessTechnologyDict.count) { - // In iOS 12, multiple radio technologies can be captured. We prefer not particular radio - // tech to another, so we'll just return the first value in the dictionary. - networkCurrentRadioAccessTechnology = networkCurrentRadioAccessTechnologyDict.allValues[0]; - } - } else { -#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MIN_REQUIRED < 120000 - networkCurrentRadioAccessTechnology = networkInfo.currentRadioAccessTechnology; -#endif // TARGET_OS_IOS && __IPHONE_OS_VERSION_MIN_REQUIRED < 120000 +#else // TARGET_OS_MACCATALYST + NSDictionary *networkCurrentRadioAccessTechnologyDict = + networkInfo.serviceCurrentRadioAccessTechnology; + if (networkCurrentRadioAccessTechnologyDict.count) { + // In iOS 12, multiple radio technologies can be captured. We prefer not particular radio + // tech to another, so we'll just return the first value in the dictionary. + networkCurrentRadioAccessTechnology = networkCurrentRadioAccessTechnologyDict.allValues[0]; } #endif // TARGET_OS_MACCATALYST if (networkCurrentRadioAccessTechnology) { @@ -149,12 +145,12 @@ GDTCORNetworkMobileSubtype GDTCORNetworkMobileSubTypeMessage() { } else { return GDTCORNetworkMobileSubtypeUNKNOWN; } -#else // TARGET_OS_IOS +#else // TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION) return GDTCORNetworkMobileSubtypeUNKNOWN; -#endif // TARGET_OS_IOS +#endif // TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION) } -NSString *_Nonnull GDTCORDeviceModel() { +NSString *_Nonnull GDTCORDeviceModel(void) { static NSString *deviceModel = @"Unknown"; #if TARGET_OS_IOS || TARGET_OS_TV @@ -182,6 +178,8 @@ GDTCORNetworkMobileSubtype GDTCORNetworkMobileSubTypeMessage() { NSError *_Nullable *error) { BOOL result = NO; if (filePath.length > 0) { + // TODO(ncooke3): For future cleanup– this API shouldn't touch the file + // system unless it successfully encoded the given object. result = [[NSFileManager defaultManager] createDirectoryAtPath:[filePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES @@ -193,80 +191,44 @@ GDTCORNetworkMobileSubtype GDTCORNetworkMobileSubTypeMessage() { } } NSData *resultData; - if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4, *)) { - resultData = [NSKeyedArchiver archivedDataWithRootObject:obj - requiringSecureCoding:YES - error:error]; - if (resultData == nil || (error != NULL && *error != nil)) { - GDTCORLogDebug(@"Encoding an object failed: %@", *error); - return nil; - } - if (filePath.length > 0) { - result = [resultData writeToFile:filePath options:NSDataWritingAtomic error:error]; - if (result == NO || *error) { + resultData = [NSKeyedArchiver archivedDataWithRootObject:obj + requiringSecureCoding:YES + error:error]; + if (resultData == nil || (error != NULL && *error != nil)) { + GDTCORLogDebug(@"Encoding an object failed: %@", *error); + return nil; + } + if (filePath.length > 0) { + result = [resultData writeToFile:filePath options:NSDataWritingAtomic error:error]; + if (result == NO || (error != NULL && *error != nil)) { + if (error != NULL && *error != nil) { GDTCORLogDebug(@"Attempt to write archive failed: path:%@ error:%@", filePath, *error); } else { - GDTCORLogDebug(@"Writing archive succeeded: %@", filePath); - } - } - } else { - @try { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - resultData = [NSKeyedArchiver archivedDataWithRootObject:obj]; -#pragma clang diagnostic pop - if (filePath.length > 0) { - result = [resultData writeToFile:filePath options:NSDataWritingAtomic error:error]; - if (result == NO || *error) { - GDTCORLogDebug(@"Attempt to write archive failed: URL:%@ error:%@", filePath, *error); - } else { - GDTCORLogDebug(@"Writing archive succeeded: %@", filePath); - } + GDTCORLogDebug(@"Attempt to write archive failed: path:%@", filePath); } - } @catch (NSException *exception) { - NSString *errorString = - [NSString stringWithFormat:@"An exception was thrown during encoding: %@", exception]; - *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:-1 - userInfo:@{NSLocalizedFailureReasonErrorKey : errorString}]; - } - if (filePath.length > 0) { - GDTCORLogDebug(@"Attempt to write archive. successful:%@ URL:%@ error:%@", - result ? @"YES" : @"NO", filePath, *error); + } else { + GDTCORLogDebug(@"Writing archive succeeded: %@", filePath); } } return resultData; } -id _Nullable GDTCORDecodeArchive(Class archiveClass, - NSString *_Nullable archivePath, - NSData *_Nullable archiveData, - NSError *_Nullable *error) { - id unarchivedObject = nil; - if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4, *)) { - NSData *data = archiveData ? archiveData : [NSData dataWithContentsOfFile:archivePath]; - if (data) { - unarchivedObject = [NSKeyedUnarchiver unarchivedObjectOfClass:archiveClass - fromData:data - error:error]; - } - } else { - @try { - NSData *archivedData = - archiveData ? archiveData : [NSData dataWithContentsOfFile:archivePath]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData]; -#pragma clang diagnostic pop - } @catch (NSException *exception) { - NSString *errorString = - [NSString stringWithFormat:@"An exception was thrown during encoding: %@", exception]; - *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:-1 - userInfo:@{NSLocalizedFailureReasonErrorKey : errorString}]; - } +id _Nullable GDTCORDecodeArchiveAtPath(Class archiveClass, + NSString *_Nonnull archivePath, + NSError **_Nonnull error) { + NSData *data = [NSData dataWithContentsOfFile:archivePath options:0 error:error]; + if (data == nil) { + // Reading the file failed and `error` will be populated. + return nil; } - return unarchivedObject; + + return GDTCORDecodeArchive(archiveClass, data, error); +} + +id _Nullable GDTCORDecodeArchive(Class archiveClass, + NSData *_Nonnull archiveData, + NSError **_Nonnull error) { + return [NSKeyedUnarchiver unarchivedObjectOfClass:archiveClass fromData:archiveData error:error]; } BOOL GDTCORWriteDataToFile(NSData *data, NSString *filePath, NSError *_Nullable *outError) { diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORProductData.m b/GoogleDataTransport/GDTCORLibrary/GDTCORProductData.m new file mode 100644 index 00000000..5e15e8fb --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORProductData.m @@ -0,0 +1,77 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h" + +@implementation GDTCORProductData + +- (instancetype)initWithProductID:(int32_t)productID { + self = [super init]; + if (self) { + _productID = productID; + } + return self; +} + +- (nonnull id)copyWithZone:(nullable NSZone *)zone { + return [[[self class] alloc] initWithProductID:self.productID]; +} + +#pragma mark - Equality + +- (BOOL)isEqualToProductData:(GDTCORProductData *)otherProductData { + return self.productID == otherProductData.productID; +} + +- (BOOL)isEqual:(nullable id)object { + if (object == nil) { + return NO; + } + + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[self class]]) { + return NO; + } + + return [self isEqualToProductData:(GDTCORProductData *)object]; +} + +- (NSUInteger)hash { + return self.productID; +} + +#pragma mark - NSSecureCoding + +/// NSCoding key for `productID` property. +static NSString *kProductIDKey = @"GDTCORProductDataProductIDKey"; + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder { + int32_t productID = [coder decodeInt32ForKey:kProductIDKey]; + return [self initWithProductID:productID]; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)coder { + [coder encodeInt32:self.productID forKey:kProductIDKey]; +} + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORRegistrar.m b/GoogleDataTransport/GDTCORLibrary/GDTCORRegistrar.m index d7e9b097..5aefd6c8 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORRegistrar.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORRegistrar.m @@ -23,7 +23,6 @@ return [GDTCORRegistrar sharedInstance].targetToStorage[@(target)]; } -FOUNDATION_EXPORT id _Nullable GDTCORStoragePromiseInstanceForTarget( GDTCORTarget target) { id storage = [GDTCORRegistrar sharedInstance].targetToStorage[@(target)]; @@ -34,14 +33,18 @@ } } -@implementation GDTCORRegistrar { - /** Backing ivar for targetToUploader property. */ - NSMutableDictionary> *_targetToUploader; - - /** Backing ivar for targetToStorage property. */ - NSMutableDictionary> *_targetToStorage; +id _Nullable GDTCORMetricsControllerInstanceForTarget( + GDTCORTarget target) { + return [GDTCORRegistrar sharedInstance].targetToMetricsController[@(target)]; } +@implementation GDTCORRegistrar + +// Manaully synthesize properties declared in `GDTCORRegistrar_Private.h` category. +@synthesize targetToUploader = _targetToUploader; +@synthesize targetToStorage = _targetToStorage; +@synthesize targetToMetricsController = _targetToMetricsController; + + (instancetype)sharedInstance { static GDTCORRegistrar *sharedInstance; static dispatch_once_t onceToken; @@ -55,8 +58,9 @@ - (instancetype)init { self = [super init]; if (self) { _registrarQueue = dispatch_queue_create("com.google.GDTCORRegistrar", DISPATCH_QUEUE_SERIAL); - _targetToUploader = [[NSMutableDictionary alloc] init]; - _targetToStorage = [[NSMutableDictionary alloc] init]; + _targetToUploader = [NSMutableDictionary dictionary]; + _targetToStorage = [NSMutableDictionary dictionary]; + _targetToMetricsController = [NSMutableDictionary dictionary]; } return self; } @@ -79,6 +83,21 @@ - (void)registerStorage:(id)storage target:(GDTCORTarget) if (strongSelf) { GDTCORLogDebug(@"Registered storage: %@ for target:%ld", storage, (long)target); strongSelf->_targetToStorage[@(target)] = storage; + [self setMetricsControllerAsStorageDelegateForTarget:target]; + } + }); +} + +- (void)registerMetricsController:(id)metricsController + target:(GDTCORTarget)target { + __weak GDTCORRegistrar *weakSelf = self; + dispatch_async(_registrarQueue, ^{ + GDTCORRegistrar *strongSelf = weakSelf; + if (strongSelf) { + GDTCORLogDebug(@"Registered metrics controller: %@ for target:%ld", metricsController, + (long)target); + strongSelf->_targetToMetricsController[@(target)] = metricsController; + [self setMetricsControllerAsStorageDelegateForTarget:target]; } }); } @@ -107,6 +126,24 @@ - (void)registerStorage:(id)storage target:(GDTCORTarget) return targetToStorage; } +- (NSMutableDictionary> *) + targetToMetricsController { + __block NSMutableDictionary> + *targetToMetricsController; + __weak GDTCORRegistrar *weakSelf = self; + dispatch_sync(_registrarQueue, ^{ + GDTCORRegistrar *strongSelf = weakSelf; + if (strongSelf) { + targetToMetricsController = strongSelf->_targetToMetricsController; + } + }); + return targetToMetricsController; +} + +- (void)setMetricsControllerAsStorageDelegateForTarget:(GDTCORTarget)target { + _targetToStorage[@(target)].delegate = _targetToMetricsController[@(target)]; +} + #pragma mark - GDTCORLifecycleProtocol - (void)appWillBackground:(nonnull GDTCORApplication *)app { diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORStorageMetadata.m b/GoogleDataTransport/GDTCORLibrary/GDTCORStorageMetadata.m new file mode 100644 index 00000000..de3b466e --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORStorageMetadata.m @@ -0,0 +1,36 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h" + +@implementation GDTCORStorageMetadata + +- (instancetype)initWithCurrentCacheSize:(GDTCORStorageSizeBytes)currentCacheSize + maxCacheSize:(GDTCORStorageSizeBytes)maxCacheSize { + self = [super init]; + if (self) { + _currentCacheSize = currentCacheSize; + _maxCacheSize = maxCacheSize; + } + return self; +} + ++ (instancetype)metadataWithCurrentCacheSize:(GDTCORStorageSizeBytes)currentCacheSize + maxCacheSize:(GDTCORStorageSizeBytes)maxCacheSize { + return [[self alloc] initWithCurrentCacheSize:currentCacheSize maxCacheSize:maxCacheSize]; +} + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORTransformer.m b/GoogleDataTransport/GDTCORLibrary/GDTCORTransformer.m index fba182ae..ece93681 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORTransformer.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORTransformer.m @@ -70,24 +70,29 @@ - (void)transformEvent:(GDTCOREvent *)event completion(wasWritten, error); } - // The work is done, cancel the background task if it's valid. - [weakApplication endBackgroundTask:bgID]; + if (bgID != GDTCORBackgroundIdentifierInvalid) { + // The work is done, cancel the background task if it's valid. + [weakApplication endBackgroundTask:bgID]; + } else { + GDTCORLog(GDTCORMCDDebugLog, GDTCORLoggingLevelWarnings, + @"Attempted to cancel invalid background task in GDTCORTransformer."); + } bgID = GDTCORBackgroundIdentifierInvalid; }; dispatch_async(_eventWritingQueue, ^{ GDTCOREvent *transformedEvent = event; for (id transformer in transformers) { - if ([transformer respondsToSelector:@selector(transform:)]) { + if ([transformer respondsToSelector:@selector(transformGDTEvent:)]) { GDTCORLogDebug(@"Applying a transformer to event %@", event); - transformedEvent = [transformer transform:transformedEvent]; + transformedEvent = [transformer transformGDTEvent:event]; if (!transformedEvent) { completionWrapper(NO, nil); return; } } else { GDTCORLogError(GDTCORMCETransformerDoesntImplementTransform, - @"Transformer doesn't implement transform: %@", transformer); + @"Transformer doesn't implement transformGDTEvent: %@", transformer); completionWrapper(NO, nil); return; } @@ -100,12 +105,4 @@ - (void)transformEvent:(GDTCOREvent *)event }); } -#pragma mark - GDTCORLifecycleProtocol - -- (void)appWillTerminate:(GDTCORApplication *)application { - // Flush the queue immediately. - dispatch_sync(_eventWritingQueue, ^{ - }); -} - @end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORTransport.m b/GoogleDataTransport/GDTCORLibrary/GDTCORTransport.m index 1db4d525..870ad710 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORTransport.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORTransport.m @@ -71,6 +71,10 @@ - (GDTCOREvent *)eventForTransport { return [[GDTCOREvent alloc] initWithMappingID:_mappingID target:_target]; } +- (GDTCOREvent *)eventForTransportWithProductData:(GDTCORProductData *)productData { + return [[GDTCOREvent alloc] initWithMappingID:_mappingID productData:productData target:_target]; +} + #pragma mark - Private helper methods /** Sends the given event through the transport pipeline. @@ -89,4 +93,16 @@ - (void)sendEvent:(GDTCOREvent *)event onComplete:completion]; } +#pragma mark - Force Category Linking + +extern void GDTCORInclude_GDTCORLogSourceMetrics_Internal_Category(void); + +/// Does nothing when called, and not meant to be called. +/// +/// This method forces the linker to include categories even if +/// users do not include the '-ObjC' linker flag in their project. ++ (void)noop { + GDTCORInclude_GDTCORLogSourceMetrics_Internal_Category(); +} + @end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORUploadCoordinator.m b/GoogleDataTransport/GDTCORLibrary/GDTCORUploadCoordinator.m index 3357fbe3..cdc108ae 100644 --- a/GoogleDataTransport/GDTCORLibrary/GDTCORUploadCoordinator.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORUploadCoordinator.m @@ -168,7 +168,7 @@ - (void)appWillBackground:(GDTCORApplication *)app { } - (void)appWillTerminate:(GDTCORApplication *)application { - dispatch_sync(_coordinationQueue, ^{ + dispatch_async(_coordinationQueue, ^{ [self stopTimer]; }); } diff --git a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h new file mode 100644 index 00000000..791678b4 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h @@ -0,0 +1,27 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +/// The reason the event was "dropped". An event is considered "dropped" when it is no longer +/// tracked by the SDK (i.e. deleted). +typedef NS_ENUM(NSInteger, GDTCOREventDropReason) { + GDTCOREventDropReasonUnknown = 0, + GDTCOREventDropReasonMessageTooOld, + GDTCOREventDropReasonStorageFull, + GDTCOREventDropReasonPayloadTooBig, + GDTCOREventDropReasonMaxRetriesReached, + GDTCOREventDropReasonInvalidPayload, + GDTCOREventDropReasonServerError +}; diff --git a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORLifecycle.h b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORLifecycle.h index 3d779707..9316e5cb 100644 --- a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORLifecycle.h +++ b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORLifecycle.h @@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN * library (GDTCORStorage and GDTCORUploadCoordinator instances) will deserialize themselves from * and to disk before and after every operation, respectively. */ -@interface GDTCORLifecycle : NSObject +@interface GDTCORLifecycle : NSObject @end diff --git a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORMetricsControllerProtocol.h b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORMetricsControllerProtocol.h new file mode 100644 index 00000000..b4a9f86f --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORMetricsControllerProtocol.h @@ -0,0 +1,57 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h" +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h" + +@class FBLPromise; +@class GDTCOREvent; +@class GDTCORMetrics; + +NS_ASSUME_NONNULL_BEGIN + +/// A storage delegate that can perform metrics related tasks. +@protocol GDTCORMetricsControllerProtocol + +/// Updates the corresponding log source metricss for the given events dropped for a given +/// reason. +/// @param reason The reason why the events are being dropped. +/// @param events The events that being dropped. +- (FBLPromise *)logEventsDroppedForReason:(GDTCOREventDropReason)reason + events:(NSSet *)events; + +/// Gets and resets the currently stored metrics. +/// @return A promise resolving with the metrics retrieved before the reset. +- (FBLPromise *)getAndResetMetrics; + +/// Offers metrics for re-storing in storage. +/// @note If the metrics are determined to be from the future, they will be ignored. +/// @param metrics The metrics to offer for storage. +- (FBLPromise *)offerMetrics:(GDTCORMetrics *)metrics; + +@end + +/// Returns a metrics controller instance for the given target. +/// @param target The target to retrieve a corresponding metrics controller from. +/// @return The given target's corresponding metrics controller instance, or `nil` if it does not +/// have one. +FOUNDATION_EXPORT +id _Nullable GDTCORMetricsControllerInstanceForTarget( + GDTCORTarget target); + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h index 44252ab6..89df0bcc 100644 --- a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h +++ b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h @@ -28,7 +28,9 @@ #import #endif // TARGET_OS_IOS || TARGET_OS_TV -#if TARGET_OS_IOS +// TODO(Xcode 15): When Xcode 15 is the minimum supported Xcode version, +// it will be unnecessary to check if `TARGET_OS_VISION` is defined. +#if TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION) #import #endif @@ -143,18 +145,23 @@ NSData *_Nullable GDTCOREncodeArchive(id obj, NSString *_Nullable filePath, NSError *_Nullable *error); -/** Decodes an object of the given class from the given archive path or data and populates the given - * error if it fails. - * - * @param archiveClass The class of the archive's root object. - * @param archivePath The path to the archived data. Don't use with the archiveData param. - * @param archiveData The data to decode. Don't use with the archivePath param. - * @param error The error to populate if something goes wrong. - */ +/// Decodes an object of the given class from the given archive path and populates the given error +/// if it fails. +/// @param archiveClass The class of the archive's root object. +/// @param archivePath The path to the archived data. +/// @param error The error to populate if something goes wrong. +id _Nullable GDTCORDecodeArchiveAtPath(Class archiveClass, + NSString *_Nonnull archivePath, + NSError **_Nonnull error); + +/// Decodes an object of the given class from the given data and populates the given error if it +/// fails. +/// @param archiveClass The class of the archive's root object. +/// @param archiveData The data to decode. +/// @param error The error to populate if something goes wrong. id _Nullable GDTCORDecodeArchive(Class archiveClass, - NSString *_Nullable archivePath, - NSData *_Nullable archiveData, - NSError *_Nullable *error); + NSData *_Nonnull archiveData, + NSError **_Nonnull error); /** Writes the provided data to a file at the provided path. Intermediate directories will be * created as needed. diff --git a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h index 3ea521b2..d5e50bbe 100644 --- a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h +++ b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h @@ -16,6 +16,7 @@ #import +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORMetricsControllerProtocol.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORUploader.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h" @@ -33,18 +34,26 @@ NS_ASSUME_NONNULL_BEGIN /** Registers a backend implementation with the GoogleDataTransport infrastructure. * - * @param backend The backend object to register. + * @param backend The backend object to register with the given target. * @param target The target this backend object will be responsible for. */ - (void)registerUploader:(id)backend target:(GDTCORTarget)target; /** Registers a storage implementation with the GoogleDataTransport infrastructure. * - * @param storage The storage instance to be associated with this uploader and target. - * @param target The target this backend object will be responsible for. + * @param storage The storage object to register with the given target. + * @param target The target this storage object will be responsible for. */ - (void)registerStorage:(id)storage target:(GDTCORTarget)target; +/** Registers a metrics controller implementation with the GoogleDataTransport infrastructure. + * + * @param metricsController The metrics controller object to register with the given target. + * @param target The target this metrics controller object will be responsible for. + */ +- (void)registerMetricsController:(id)metricsController + target:(GDTCORTarget)target; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h index dcb451e9..2a3ae0e9 100644 --- a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h +++ b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h @@ -18,6 +18,7 @@ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORLifecycle.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageEventSelector.h" +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageSizeBytes.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h" @class GDTCOREvent; @@ -26,17 +27,21 @@ @class FBLPromise; -NS_ASSUME_NONNULL_BEGIN +@protocol GDTCORStorageDelegate; -/** The data type to represent storage size. */ -typedef uint64_t GDTCORStorageSizeBytes; +NS_ASSUME_NONNULL_BEGIN typedef void (^GDTCORStorageBatchBlock)(NSNumber *_Nullable newBatchID, NSSet *_Nullable batchEvents); +#pragma mark - GDTCORStorageProtocol + /** Defines the interface a storage subsystem is expected to implement. */ @protocol GDTCORStorageProtocol +/// The object that acts as the delegate of the storage instance. +@property(nonatomic, weak, nullable) id delegate; + @required /** Stores an event and calls onComplete with a non-nil error if anything went wrong. @@ -123,9 +128,14 @@ typedef void (^GDTCORStorageBatchBlock)(NSNumber *_Nullable newBatchID, @end -// TODO: Consider complete replacing block based API by promise API. +#pragma mark - GDTCORStoragePromiseProtocol -/** Promise based version of API defined in GDTCORStorageProtocol. See API docs for corresponding +// TODO(ncooke3): Consider complete replacing block based API by promise API. + +@class GDTCORMetricsMetadata; +@class GDTCORStorageMetadata; + +/** Promise based version of API defined in GDTCORStorageProtocol. See API docs for corresponding * methods in GDTCORStorageProtocol. */ @protocol GDTCORStoragePromiseProtocol @@ -139,6 +149,18 @@ typedef void (^GDTCORStorageBatchBlock)(NSNumber *_Nullable newBatchID, - (FBLPromise *)removeAllBatchesForTarget:(GDTCORTarget)target deleteEvents:(BOOL)deleteEvents; +/// Fetches metrics metadata from storage, passes them to the given handler, and writes the +/// resulting metrics metadata from the given handler to storage. +/// @note This API is thread-safe. +/// @param handler A handler to process the fetch result and return an updated value to store. +/// @return A promise that is fulfilled if the update is successful, and rejected otherwise. +- (FBLPromise *)fetchAndUpdateMetricsWithHandler: + (GDTCORMetricsMetadata * (^)(GDTCORMetricsMetadata *_Nullable fetchedMetadata, + NSError *_Nullable fetchError))handler; + +/// Fetches and returns storage metadata. +- (FBLPromise *)fetchStorageMetadata; + /** See `hasEventsForTarget:onComplete:`. * @return A promise object that is resolved with @YES if there are events for the specified target * and @NO otherwise. @@ -162,10 +184,27 @@ typedef void (^GDTCORStorageBatchBlock)(NSNumber *_Nullable newBatchID, FOUNDATION_EXPORT id _Nullable GDTCORStorageInstanceForTarget(GDTCORTarget target); -// TODO: Ideally we should remove completion-based API and use promise-based one. Need to double -// check if it's ok. FOUNDATION_EXPORT id _Nullable GDTCORStoragePromiseInstanceForTarget( GDTCORTarget target); +#pragma mark - GDTCORStorageDelegate + +/// A type that can be delegated actions from a storage instance. +@protocol GDTCORStorageDelegate + +/// Tells the delegate that the storage instance has removed a set of expired events. +/// @param storage The storage instance informing the delegate of this impending event. +/// @param events A set of events that were removed from storage due to their expiration. +- (void)storage:(id)storage + didRemoveExpiredEvents:(NSSet *)events; + +/// Tells the delegate that the storage instance has dropped an event due to the event cache being +/// full. +/// @param storage The storage instance informing the delegate of this impending event. +/// @param event An event that was dropped due to the event cache being full. +- (void)storage:(id)storage didDropEvent:(GDTCOREvent *)event; + +@end + NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageSizeBytes.h b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageSizeBytes.h new file mode 100644 index 00000000..4b24f959 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageSizeBytes.h @@ -0,0 +1,18 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +/// The data type to represent storage size. +typedef uint64_t GDTCORStorageSizeBytes; diff --git a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h new file mode 100644 index 00000000..8722073b --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h @@ -0,0 +1,53 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h" + +@class GDTCOREvent; + +NS_ASSUME_NONNULL_BEGIN + +/// A model object that tracks, per log source, the number of events dropped for a variety of +/// reasons. An event is considered "dropped" when the event is no longer persisted by the SDK. +@interface GDTCORLogSourceMetrics : NSObject + +/// Creates an empty log source metrics instance. ++ (instancetype)metrics; + +/// Creates a log source metrics for a collection of events that were dropped for a given reason. +/// @param events The collection of events that were dropped. +/// @param reason The reason for which given events were dropped. ++ (instancetype)metricsWithEvents:(NSArray *)events + droppedForReason:(GDTCOREventDropReason)reason; + +/// This API is unavailable. +- (instancetype)init NS_UNAVAILABLE; + +/// Returns a log source metrics instance created by merging the receiving log +/// source metrics with the given log source metrics. +/// @param logSourceMetrics The given log source metrics to merge with. +- (GDTCORLogSourceMetrics *)logSourceMetricsByMergingWithLogSourceMetrics: + (GDTCORLogSourceMetrics *)logSourceMetrics; + +/// Returns a Boolean value that indicates whether the receiving log source metrics is equal to +/// the given log source metrics. +/// @param otherLogSourceMetrics The log source metrics with which to compare the +/// receiving log source metrics. +- (BOOL)isEqualToLogSourceMetrics:(GDTCORLogSourceMetrics *)otherLogSourceMetrics; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h new file mode 100644 index 00000000..2f3c38fc --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h @@ -0,0 +1,61 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventDataObject.h" + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageSizeBytes.h" + +@class GDTCORLogSourceMetrics; +@class GDTCORMetricsMetadata; +@class GDTCORStorageMetadata; + +NS_ASSUME_NONNULL_BEGIN + +/// An object representing metrics that represent a snapshot of the SDK's state and performance. +@interface GDTCORMetrics : NSObject + +/// The start of the time window over which the metrics were collected. +@property(nonatomic, readonly) NSDate *collectionStartDate; + +/// The log source metrics associated with the metrics. +@property(nonatomic, readonly) GDTCORLogSourceMetrics *logSourceMetrics; + +/// The end of the time window over which the metrics were collected. +@property(nonatomic, readonly) NSDate *collectionEndDate; + +/// The number of bytes the event cache was consuming in storage. +@property(nonatomic, readonly) GDTCORStorageSizeBytes currentCacheSize; + +/// The maximum number of bytes that the event cache is allowed to grow. +@property(nonatomic, readonly) GDTCORStorageSizeBytes maxCacheSize; + +/// The bundle ID associated with the metrics being collected. +@property(nonatomic, readonly) NSString *bundleID; + +/// Creates a metrics instance with the provided metadata. +/// @param metricsMetadata The provided metrics metadata. +/// @param storageMetadata The provided storage metadata. ++ (instancetype)metricsWithMetricsMetadata:(GDTCORMetricsMetadata *)metricsMetadata + storageMetadata:(GDTCORStorageMetadata *)storageMetadata; + +/// Returns a Boolean value that indicates whether the receiving metrics is equal to the given +/// metrics. +/// @param otherMetrics The metrics with which to compare the receiving metrics. +- (BOOL)isEqualToMetrics:(GDTCORMetrics *)otherMetrics; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsController.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsController.h new file mode 100644 index 00000000..a47da529 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsController.h @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORMetricsControllerProtocol.h" + +@protocol GDTCORStoragePromiseProtocol; + +NS_ASSUME_NONNULL_BEGIN + +@interface GDTCORMetricsController : NSObject + +/// Returns the event metrics controller singleton. ++ (instancetype)sharedInstance; + +/// Designated initializer. +/// @param storage The storage object to read and write metrics data from. +- (instancetype)initWithStorage:(id)storage NS_DESIGNATED_INITIALIZER; + +/// This API is unavailable. +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h new file mode 100644 index 00000000..8214c413 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h @@ -0,0 +1,50 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h" + +@class GDTCORLogSourceMetrics; + +NS_ASSUME_NONNULL_BEGIN + +/// An encodable model object that contains metadata that is persisted in storage until ready to be +/// used to create a ``GDTCORMetrics`` instance. +@interface GDTCORMetricsMetadata : NSObject + +/// The start of the time window over which the metrics were collected. +@property(nonatomic, copy, readonly) NSDate *collectionStartDate; + +/// The log source metrics associated with the metrics. +@property(nonatomic, copy, readonly) GDTCORLogSourceMetrics *logSourceMetrics; + +/// Creates a metrics metadata object with the provided information. +/// @param collectedSinceDate The start of the time window over which the metrics were collected. +/// @param logSourceMetrics The metrics object that tracks metrics for each log source. ++ (instancetype)metadataWithCollectionStartDate:(NSDate *)collectedSinceDate + logSourceMetrics:(GDTCORLogSourceMetrics *)logSourceMetrics; + +/// This API is unavailable. +- (instancetype)init NS_UNAVAILABLE; + +/// Returns a Boolean value that indicates whether the receiving metrics metadata is equal to +/// the given metrics metadata. +/// @param otherMetricsMetadata The metrics metadata with which to compare the +/// receiving metrics metadata. +- (BOOL)isEqualToMetricsMetadata:(GDTCORMetricsMetadata *)otherMetricsMetadata; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h index 26796110..9f455487 100644 --- a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h @@ -30,6 +30,10 @@ NS_ASSUME_NONNULL_BEGIN @property(atomic, readonly) NSMutableDictionary> *targetToStorage; +/** A map of targets to metrics controller instances. */ +@property(atomic, readonly) + NSMutableDictionary> *targetToMetricsController; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h new file mode 100644 index 00000000..90b529e4 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h @@ -0,0 +1,41 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageSizeBytes.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A model object that contains metadata about the current state of the SDK's storage container. +@interface GDTCORStorageMetadata : NSObject + +/// The number of bytes the event cache is consuming in storage. +@property(nonatomic, readonly) GDTCORStorageSizeBytes currentCacheSize; + +/// The maximum number of bytes that the event cache may consume in storage. +@property(nonatomic, readonly) GDTCORStorageSizeBytes maxCacheSize; + +/// Creates a storage metadata object with the provided information. +/// @param currentCacheSize The current number of bytes the event cache is consuming. +/// @param maxCacheSize The current maximum capacity (in bytes) that the event cache may consume. ++ (instancetype)metadataWithCurrentCacheSize:(GDTCORStorageSizeBytes)currentCacheSize + maxCacheSize:(GDTCORStorageSizeBytes)maxCacheSize; + +/// This API is unavailable. +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer.h index 2868306a..ccca6283 100644 --- a/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer.h @@ -38,7 +38,8 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)sharedInstance; -/** Writes the result of applying the given transformers' -transform method on the given event. +/** Writes the result of applying the given transformers' `transformGDTEvent:` method on the given + * event. * * @note If the app is suspended, a background task will be created to complete work in-progress, * but this method will not send any further events until the app is resumed. diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h index d306f182..1fdf7325 100644 --- a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h @@ -68,7 +68,8 @@ typedef NS_ENUM(NSInteger, GDTCORMessageCode) { /** For warning messages concerning the reading of a event file. */ GDTCORMCWFileReadError = 6, - /** For error messages concerning transform: not being implemented by an event transformer. */ + /** For error messages concerning transformGDTEvent: not being implemented by an event + transformer. */ GDTCORMCETransformerDoesntImplementTransform = 1000, /** For error messages concerning the creation of a directory failing. */ diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h index 52c2384e..8ab36014 100644 --- a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h @@ -20,6 +20,7 @@ #import "GDTCORTargets.h" @class GDTCORClock; +@class GDTCORProductData; NS_ASSUME_NONNULL_BEGIN @@ -74,7 +75,10 @@ typedef NS_ENUM(NSInteger, GDTCOREventQoS) { /** Bytes that can be used by an uploader later on. */ @property(nullable, nonatomic) NSData *customBytes; -/** Initializes an instance using the given mappingID. +/** The product data that the event is associated with, if any. */ +@property(nullable, readonly, nonatomic) GDTCORProductData *productData; + +/** Initializes an instance using the given mapping ID and target. * * @param mappingID The mapping identifier. * @param target The event's target identifier. @@ -82,6 +86,17 @@ typedef NS_ENUM(NSInteger, GDTCOREventQoS) { */ - (nullable instancetype)initWithMappingID:(NSString *)mappingID target:(GDTCORTarget)target; +/** Initializes an instance using the given mapping ID, product data, and target. + * + * @param mappingID The mapping identifier. + * @param productData The product data to associate this event with. + * @param target The event's target identifier. + * @return An instance of this class. + */ +- (nullable instancetype)initWithMappingID:(NSString *)mappingID + productData:(nullable GDTCORProductData *)productData + target:(GDTCORTarget)target; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventTransformer.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventTransformer.h index 29f95924..80dee7d4 100644 --- a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventTransformer.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREventTransformer.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN * @param event The event to transform. * @return A transformed event, or nil if the transformation dropped the event. */ -- (nullable GDTCOREvent *)transform:(GDTCOREvent *)event; +- (nullable GDTCOREvent *)transformGDTEvent:(GDTCOREvent *)event; @end diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h new file mode 100644 index 00000000..ff99822e --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h @@ -0,0 +1,29 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@interface GDTCORProductData : NSObject + +/// The product ID. +@property(nonatomic, readonly) int32_t productID; + +/// Initializes an instance using the given product ID. +/// - Parameter productID: A 32-bit integer. +- (instancetype)initWithProductID:(int32_t)productID; + +/// This API is unavailable. +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h index 0b83ab9e..3163b550 100644 --- a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h @@ -16,25 +16,24 @@ #import -/** The list of targets supported by the shared transport infrastructure. If adding a new target, - * please use the previous value +1. +/** The list of targets supported by the shared transport infrastructure. + * These targets map to a specific backend designed to accept GDT payloads. If + * adding a new target, please use the previous value +1. */ typedef NS_ENUM(NSInteger, GDTCORTarget) { - /** A target only used in testing. */ + /** Target used for testing purposes. */ kGDTCORTargetTest = 999, - /** The CCT target. */ + /** Target used by internal clients. See go/firelog for more information. */ kGDTCORTargetCCT = 1000, - /** The FLL target. */ + /** Target mapping to the Firelog backend. See go/firelog for more information. */ kGDTCORTargetFLL = 1001, - /** The CSH target. The CSH target is a special-purpose backend. Please do not use it without - * permission. - */ + /** Special-purpose Crashlytics target. Please do not use it without permission. */ kGDTCORTargetCSH = 1002, - /** The INT target. */ + /** Target used for integration testing. */ kGDTCORTargetINT = 1003, }; diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTransport.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTransport.h index e58248da..33d1ba25 100644 --- a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTransport.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTransport.h @@ -20,6 +20,7 @@ #import "GDTCORTargets.h" @class GDTCOREvent; +@class GDTCORProductData; NS_ASSUME_NONNULL_BEGIN @@ -87,6 +88,14 @@ NS_ASSUME_NONNULL_BEGIN */ - (GDTCOREvent *)eventForTransport; +/** + * Creates an event with the given product data for use by this transport. + * + * @param productData The given product data to associate with the created event. + * @return An event that is suited for use by this transport. + */ +- (GDTCOREvent *)eventForTransportWithProductData:(nonnull GDTCORProductData *)productData; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GoogleDataTransport.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GoogleDataTransport.h index 0bd39dc0..b5e83d1d 100644 --- a/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GoogleDataTransport.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GoogleDataTransport.h @@ -20,5 +20,6 @@ #import "GDTCOREvent.h" #import "GDTCOREventDataObject.h" #import "GDTCOREventTransformer.h" +#import "GDTCORProductData.h" #import "GDTCORTargets.h" #import "GDTCORTransport.h" diff --git a/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.m b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.m index a78c7b69..f1e5be2f 100644 --- a/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.m +++ b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.m @@ -24,6 +24,7 @@ - (void)reset { // These calls occur on the queue. [self.targetToUploader removeAllObjects]; [self.targetToStorage removeAllObjects]; + [self.targetToMetricsController removeAllObjects]; dispatch_sync(self.registrarQueue, ^{ }); } diff --git a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.h b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.h new file mode 100644 index 00000000..88dbaea8 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.h @@ -0,0 +1,44 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h" +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORMetricsControllerProtocol.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h" + +@class FBLPromise; +@class GDTCOREvent; +@class GDTCORMetrics; + +NS_ASSUME_NONNULL_BEGIN + +@interface GDTCORMetricsControllerFake : NSObject + +@property(nonatomic, copy, nullable) void (^onLogEventsDroppedHandler) + (GDTCOREventDropReason reason, NSSet *events); + +@property(nonatomic, copy, nullable) FBLPromise * (^onGetAndResetMetricsHandler) + (void); + +@property(nonatomic, copy, nullable) void (^onOfferMetricsHandler)(GDTCORMetrics *metrics); + +@property(nonatomic, copy, nullable) void (^onStorageDidDropEvent)(GDTCOREvent *event); + +@property(nonatomic, copy, nullable) void (^onStorageDidRemoveExpiredEvents) + (NSSet *events); + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.m b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.m new file mode 100644 index 00000000..b221d639 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.m @@ -0,0 +1,70 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.h" + +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +@implementation GDTCORMetricsControllerFake + +- (FBLPromise *)logEventsDroppedForReason:(GDTCOREventDropReason)reason + events:(NSSet *)events { + if (self.onLogEventsDroppedHandler) { + self.onLogEventsDroppedHandler(reason, events); + } else { + [self doesNotRecognizeSelector:_cmd]; + } + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)getAndResetMetrics { + if (self.onGetAndResetMetricsHandler) { + return self.onGetAndResetMetricsHandler(); + } else { + [self doesNotRecognizeSelector:_cmd]; + return [FBLPromise resolvedWith:nil]; + } +} + +- (FBLPromise *)offerMetrics:(GDTCORMetrics *)metrics { + if (self.onOfferMetricsHandler) { + self.onOfferMetricsHandler(metrics); + } else { + [self doesNotRecognizeSelector:_cmd]; + } + return [FBLPromise resolvedWith:nil]; +} + +- (void)storage:(id)storage didDropEvent:(GDTCOREvent *)event { + if (self.onStorageDidDropEvent) { + return self.onStorageDidDropEvent(event); + } else { + [self doesNotRecognizeSelector:_cmd]; + } +} + +- (void)storage:(nonnull id)storage + didRemoveExpiredEvents:(nonnull NSSet *)events { + if (self.onStorageDidRemoveExpiredEvents) { + return self.onStorageDidRemoveExpiredEvents(events); + } else { + [self doesNotRecognizeSelector:_cmd]; + } +} + +@end diff --git a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h index 1d4edf56..2a3b771b 100644 --- a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h @@ -19,7 +19,10 @@ NS_ASSUME_NONNULL_BEGIN /** A functionless fake that can be injected into classes that need it. */ -@interface GDTCORStorageFake : NSObject +@interface GDTCORStorageFake : NSObject + +/// Creates an storage fake. ++ (instancetype)storageFake; @end diff --git a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m index c9eb6fd0..cb596e60 100644 --- a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m @@ -16,11 +16,27 @@ #import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h" +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h" + @implementation GDTCORStorageFake { - /** Store the events in memory. */ + /// Store the events in memory. NSMutableDictionary *_storedEvents; + /// Store the metrics metadata in memory. + GDTCORMetricsMetadata *_Nullable _storedMetricsMetadata; +} + +@synthesize delegate = _delegate; + ++ (instancetype)storageFake { + return [[self alloc] init]; } - (void)storeEvent:(GDTCOREvent *)event @@ -90,4 +106,54 @@ - (void)batchIDsForTarget:(GDTCORTarget)target - (void)checkForExpirations { } +- (nonnull FBLPromise *> *)batchIDsForTarget:(GDTCORTarget)target { + NSAssert(NO, @"This API should be implemented if this fake is used in tests."); + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)batchWithEventSelector: + (nonnull GDTCORStorageEventSelector *)eventSelector + batchExpiration:(nonnull NSDate *)expiration { + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)fetchAndUpdateMetricsWithHandler: + (nonnull GDTCORMetricsMetadata * (^)(GDTCORMetricsMetadata *_Nullable, + NSError *_Nullable))handler { + if (_storedMetricsMetadata != nil) { + _storedMetricsMetadata = handler(_storedMetricsMetadata, nil); + } else { + NSError *emptyStorageError = [NSError errorWithDomain:@"GDTCORStorageFake" code:1 userInfo:nil]; + _storedMetricsMetadata = handler(nil, emptyStorageError); + } + + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)fetchStorageMetadata { + GDTCORStorageMetadata *storageMetadata = + [GDTCORStorageMetadata metadataWithCurrentCacheSize:15 * 1000 * 1000 // 15 MB + maxCacheSize:20 * 1000 * 1000]; // 20 MB + return [FBLPromise resolvedWith:storageMetadata]; +} + +- (nonnull FBLPromise *)hasEventsForTarget:(GDTCORTarget)target { + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)removeAllBatchesForTarget:(GDTCORTarget)target + deleteEvents:(BOOL)deleteEvents { + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)removeBatchWithID:(nonnull NSNumber *)batchID + deleteEvents:(BOOL)deleteEvents { + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)removeBatchesWithIDs:(nonnull NSSet *)batchIDs + deleteEvents:(BOOL)deleteEvents { + return [FBLPromise resolvedWith:nil]; +} + @end diff --git a/GoogleDataTransport/GDTCORTests/Integration/GDTCORIntegrationTest.m b/GoogleDataTransport/GDTCORTests/Integration/GDTCORIntegrationTest.m index 80a3338d..9b32ff42 100644 --- a/GoogleDataTransport/GDTCORTests/Integration/GDTCORIntegrationTest.m +++ b/GoogleDataTransport/GDTCORTests/Integration/GDTCORIntegrationTest.m @@ -52,8 +52,8 @@ @interface GDTCORIntegrationTestTransformer : NSObject @implementation GDTCORIntegrationTestTransformer -- (nullable GDTCOREvent *)transform:(GDTCOREvent *)event { - // drop half the events during transforming. +- (nullable GDTCOREvent *)transformGDTEvent:(GDTCOREvent *)event { + // Drop half the events during transforming. if (arc4random_uniform(2) == 0) { event = nil; } diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORClockTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORClockTest.m index 7cdd4ce2..26eb8a67 100644 --- a/GoogleDataTransport/GDTCORTests/Unit/GDTCORClockTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORClockTest.m @@ -59,7 +59,7 @@ - (void)testEncoding { error = nil; GDTCORClock *unarchivedClock = - (GDTCORClock *)GDTCORDecodeArchive([GDTCORClock class], nil, clockData, &error); + (GDTCORClock *)GDTCORDecodeArchive([GDTCORClock class], clockData, &error); XCTAssertNil(error); XCTAssertNotNil(unarchivedClock); XCTAssertEqual([clock hash], [unarchivedClock hash]); diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCOREventTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCOREventTest.m index f01b1749..09bbaa97 100644 --- a/GoogleDataTransport/GDTCORTests/Unit/GDTCOREventTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCOREventTest.m @@ -15,6 +15,7 @@ */ #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" @@ -34,42 +35,42 @@ @interface GDTCOREventTest : GDTCORTestCase @implementation GDTCOREventTest -/** Tests the designated initializer. */ +/** Tests initializers. */ - (void)testInit { + // Test `- [GDTCOREvent initWithMappingID:target:]` XCTAssertNotNil([[GDTCOREvent alloc] initWithMappingID:@"1" target:kGDTCORTargetTest].eventID); XCTAssertNotNil([[GDTCOREvent alloc] initWithMappingID:@"1" target:kGDTCORTargetTest]); XCTAssertNil([[GDTCOREvent alloc] initWithMappingID:@"" target:kGDTCORTargetTest]); + // Test `- [GDTCOREvent initWithMappingID:productData:target:]` + GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:123456]; + GDTCOREvent *event1 = [[GDTCOREvent alloc] initWithMappingID:@"1" + productData:productData + target:kGDTCORTargetTest]; + XCTAssertNotNil(event1); + XCTAssertNotNil(event1.productData); + XCTAssertEqual(event1.productData.productID, 123456); + GDTCOREvent *event2 = [[GDTCOREvent alloc] initWithMappingID:@"1" + productData:nil + target:kGDTCORTargetTest]; + XCTAssertNotNil(event2); + XCTAssertNil(event2.productData); } /** Tests NSKeyedArchiver encoding and decoding. */ - (void)testArchiving { - XCTAssertTrue([GDTCOREvent supportsSecureCoding]); - GDTCORClock *clockSnapshot = [GDTCORClock snapshot]; - int64_t timeMillis = clockSnapshot.timeMillis; - int64_t timezoneOffsetSeconds = clockSnapshot.timezoneOffsetSeconds; + // Test round trip archive of event with `nil` product data. GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"testID" target:kGDTCORTargetTest]; - event.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"someData"]; - event.qosTier = GDTCOREventQoSTelemetry; - event.clockSnapshot = clockSnapshot; - - NSError *error; - NSData *archiveData = GDTCOREncodeArchive(event, nil, &error); - XCTAssertNil(error); - XCTAssertNotNil(archiveData); - - // To ensure that all the objects being retained by the original event are dealloc'd. - event = nil; - error = nil; - GDTCOREvent *decodedEvent = - (GDTCOREvent *)GDTCORDecodeArchive([GDTCOREvent class], nil, archiveData, &error); - XCTAssertNil(error); - XCTAssertNotNil(decodedEvent); - XCTAssertEqualObjects(decodedEvent.mappingID, @"testID"); - XCTAssertEqual(decodedEvent.target, kGDTCORTargetTest); - event.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"someData"]; - XCTAssertEqual(decodedEvent.qosTier, GDTCOREventQoSTelemetry); - XCTAssertEqual(decodedEvent.clockSnapshot.timeMillis, timeMillis); - XCTAssertEqual(decodedEvent.clockSnapshot.timezoneOffsetSeconds, timezoneOffsetSeconds); + GDTCOREvent *roundTripEvent = [self assertArchiveUnarchiveRoundTripForEvent:event]; + XCTAssertEqualObjects(event, roundTripEvent); + + // Test round trip archive of event with non-`nil` product data. + GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:123456]; + GDTCOREvent *eventWithProductData = [[GDTCOREvent alloc] initWithMappingID:@"testID" + productData:productData + target:kGDTCORTargetTest]; + GDTCOREvent *roundTripEventWithProductData = + [self assertArchiveUnarchiveRoundTripForEvent:eventWithProductData]; + XCTAssertEqualObjects(eventWithProductData, roundTripEventWithProductData); } /** Tests setting variables on a GDTCOREvent instance.*/ @@ -88,34 +89,18 @@ - (void)testSettingVariables { /** Tests equality between GDTCOREvents. */ - (void)testIsEqualAndHash { - GDTCOREvent *event1 = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:kGDTCORTargetTest]; - event1.eventID = @"123"; - event1.clockSnapshot = [GDTCORClock snapshot]; - [event1.clockSnapshot setValue:@(1553534573010) forKeyPath:@"timeMillis"]; - [event1.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; - [event1.clockSnapshot setValue:@(1552576634359451) forKeyPath:@"kernelBootTimeNanoseconds"]; - [event1.clockSnapshot setValue:@(961141365197) forKeyPath:@"uptimeNanoseconds"]; - event1.qosTier = GDTCOREventQosDefault; - event1.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"someData"]; NSError *error1; - event1.customBytes = [NSJSONSerialization dataWithJSONObject:@{@"customParam1" : @"aValue1"} - options:0 - error:&error1]; + GDTCOREvent *event1 = [self eventWithMappingID:@"1018" + productData:nil + target:kGDTCORTargetTest + error:&error1]; XCTAssertNil(error1); - GDTCOREvent *event2 = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:kGDTCORTargetTest]; - event2.eventID = @"123"; - event2.clockSnapshot = [GDTCORClock snapshot]; - [event2.clockSnapshot setValue:@(1553534573010) forKeyPath:@"timeMillis"]; - [event2.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; - [event2.clockSnapshot setValue:@(1552576634359451) forKeyPath:@"kernelBootTimeNanoseconds"]; - [event2.clockSnapshot setValue:@(961141365197) forKeyPath:@"uptimeNanoseconds"]; - event2.qosTier = GDTCOREventQosDefault; - event2.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"someData"]; NSError *error2; - event2.customBytes = [NSJSONSerialization dataWithJSONObject:@{@"customParam1" : @"aValue1"} - options:0 - error:&error2]; + GDTCOREvent *event2 = [self eventWithMappingID:@"1018" + productData:nil + target:kGDTCORTargetTest + error:&error2]; XCTAssertNil(error2); XCTAssertEqual([event1 hash], [event2 hash]); XCTAssertEqualObjects(event1, event2); @@ -125,6 +110,15 @@ - (void)testIsEqualAndHash { XCTAssertNotEqual([event1 hash], [event2 hash]); XCTAssertNotEqualObjects(event1, event2); + + // This only really tests that changing the `productData` value causes a change in hash. + NSError *error3; + GDTCOREvent *event3 = [self eventWithMappingID:@"1018" + productData:[[GDTCORProductData alloc] initWithProductID:98765] + target:kGDTCORTargetTest + error:&error3]; + XCTAssertNil(error3); + XCTAssertNotEqual([event1 hash], [event3 hash]); } /** Tests generating event IDs. */ @@ -141,4 +135,57 @@ - (void)testGenerateEventIDs { self.continueAfterFailure = originalContinueAfterFailureValue; } +/** Archives the given event and returns its unarchived form. */ +- (GDTCOREvent *)assertArchiveUnarchiveRoundTripForEvent:(GDTCOREvent *)event { + XCTAssertTrue([GDTCOREvent supportsSecureCoding]); + GDTCORClock *clockSnapshot = [GDTCORClock snapshot]; + int64_t timeMillis = clockSnapshot.timeMillis; + int64_t timezoneOffsetSeconds = clockSnapshot.timezoneOffsetSeconds; + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"someData"]; + event.qosTier = GDTCOREventQoSTelemetry; + event.clockSnapshot = clockSnapshot; + + NSError *error; + NSData *archiveData = GDTCOREncodeArchive(event, nil, &error); + XCTAssertNil(error); + XCTAssertNotNil(archiveData); + + // To ensure that all the objects being retained by the original event are dealloc'd. + event = nil; + error = nil; + GDTCOREvent *decodedEvent = + (GDTCOREvent *)GDTCORDecodeArchive([GDTCOREvent class], archiveData, &error); + XCTAssertNil(error); + XCTAssertNotNil(decodedEvent); + XCTAssertEqualObjects(decodedEvent.mappingID, @"testID"); + XCTAssertEqual(decodedEvent.target, kGDTCORTargetTest); + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"someData"]; + XCTAssertEqual(decodedEvent.qosTier, GDTCOREventQoSTelemetry); + XCTAssertEqual(decodedEvent.clockSnapshot.timeMillis, timeMillis); + XCTAssertEqual(decodedEvent.clockSnapshot.timezoneOffsetSeconds, timezoneOffsetSeconds); + return decodedEvent; +} + +/** Creates an event with a set snapshot time. */ +- (GDTCOREvent *)eventWithMappingID:(NSString *)mappingID + productData:(nullable GDTCORProductData *)productData + target:(GDTCORTarget)target + error:(NSError **)outError { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" + productData:productData + target:kGDTCORTargetTest]; + event.eventID = @"123"; + event.clockSnapshot = [GDTCORClock snapshot]; + [event.clockSnapshot setValue:@(1553534573010) forKeyPath:@"timeMillis"]; + [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; + [event.clockSnapshot setValue:@(1552576634359451) forKeyPath:@"kernelBootTimeNanoseconds"]; + [event.clockSnapshot setValue:@(961141365197) forKeyPath:@"uptimeNanoseconds"]; + event.qosTier = GDTCOREventQosDefault; + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"someData"]; + event.customBytes = [NSJSONSerialization dataWithJSONObject:@{@"customParam1" : @"aValue1"} + options:0 + error:outError]; + return event; +} + @end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORFlatFileStorageTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORFlatFileStorageTest.m index f801e059..561f3dc1 100644 --- a/GoogleDataTransport/GDTCORTests/Unit/GDTCORFlatFileStorageTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORFlatFileStorageTest.m @@ -16,18 +16,25 @@ #import "GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h" +#import "FBLPromise+Testing.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORFlatFileStorage+Promises.h" #import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORFlatFileStorage.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" #import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h" #import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h" #import "GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h" #import "GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.h" #import "GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h" #import "GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" +#import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.h" #import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.h" #import "GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORFlatFileStorage+Testing.h" @@ -207,7 +214,10 @@ - (void)testStoreMultipleEvents { }]); [self waitForExpectations:@[ writtenExpectation ] timeout:10.0]; - GDTCOREvent *event3 = [[GDTCOREvent alloc] initWithMappingID:@"404" target:kGDTCORTargetTest]; + GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:98765]; + GDTCOREvent *event3 = [[GDTCOREvent alloc] initWithMappingID:@"404" + productData:productData + target:kGDTCORTargetTest]; event3.dataObject = [[GDTCORDataObjectTesterSimple alloc] initWithString:@"testString3"]; writtenExpectation = [self expectationWithDescription:@"event written"]; XCTAssertNoThrow([storage storeEvent:event3 @@ -226,6 +236,9 @@ - (void)testStoreMultipleEvents { onComplete:^(NSNumber *_Nullable batchID, NSSet *_Nullable events) { XCTAssertEqual(events.count, 3); + XCTAssert([events containsObject:event1]); + XCTAssert([events containsObject:event2]); + XCTAssert([events containsObject:event3]); [expectation fulfill]; }]; [self waitForExpectations:@[ expectation ] timeout:10]; @@ -292,6 +305,140 @@ - (void)testStoringEventsDuringTerminate { self.continueAfterFailure = originalValueOfContinueAfterFailure; } +- (void)testFetchAndUpdateMetrics { + // Given + GDTCORMetricsMetadata *metricsMetadata = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate date] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + + __auto_type fetchAndUpdatePromise1 = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNil(fetchedMetadata); + XCTAssertNotNil(fetchError); + return metricsMetadata; + }]; + + // When + __auto_type fetchAndUpdatePromise2 = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNotNil(fetchedMetadata); + XCTAssertNil(fetchError); + XCTAssertEqualObjects(fetchedMetadata, metricsMetadata); + return metricsMetadata; + }]; + + // Then + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(fetchAndUpdatePromise1.isFulfilled); + XCTAssert(fetchAndUpdatePromise2.isFulfilled); +} + +- (void)testFetchAndUpdateMetrics_WhenDecodeError { + // When + __auto_type fetchAndUpdatePromise = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNil(fetchedMetadata); + XCTAssertNotNil(fetchError); + XCTAssertEqualObjects(fetchError.localizedFailureReason, @"The file doesn’t exist."); + return fetchedMetadata; + }]; + + // Then + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(fetchAndUpdatePromise.isFulfilled); +} + +- (void)testFetchAndUpdateMetrics_WhenUpdatedMetadataIsUnexpected_DoesNothing { + // Given + GDTCORMetricsMetadata *metricsMetadata = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate date] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + + __auto_type fetchAndUpdatePromise1 = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNil(fetchedMetadata); + XCTAssertNotNil(fetchError); + return metricsMetadata; + }]; + + // When + __auto_type fetchAndUpdatePromise2 = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNotNil(fetchedMetadata); + XCTAssertNil(fetchError); + return fetchedMetadata; + }]; + + // Then + __auto_type fetchAndUpdatePromise3 = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNotNil(fetchedMetadata); + XCTAssertNil(fetchError); + XCTAssertEqualObjects(fetchedMetadata, metricsMetadata); + return fetchedMetadata; + }]; + + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(fetchAndUpdatePromise1.isFulfilled); + XCTAssert(fetchAndUpdatePromise2.isFulfilled); + XCTAssert(fetchAndUpdatePromise3.isFulfilled); +} + +- (void)testFetchAndUpdateMetrics_WhenUpdateFails_RejectsPromise { + // When + __auto_type fetchAndUpdatePromise = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNil(fetchedMetadata); + XCTAssertNotNil(fetchError); + // Return an object that doesn't conform to `NSCoding` so the update + // will fail because the "GDTCORMetricsMetadata" cannot be encoded. + return (GDTCORMetricsMetadata *)[[NSObject alloc] init]; + }]; + + // Then + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(fetchAndUpdatePromise.isRejected); +} + +- (void)testMetricsStorageLocationRegressions { + // Given + // - Initially, there should be no library data stored. + NSString *metricsMetadataPath = [GDTCORFlatFileStorage.libraryDataStoragePath + stringByAppendingPathComponent:@"metrics_metadata"]; + NSError *error; + XCTAssertNil([[NSFileManager defaultManager] contentsOfDirectoryAtPath:metricsMetadataPath + error:&error]); + XCTAssertNotNil(error); + + // When + __auto_type fetchAndUpdatePromise = [GDTCORFlatFileStorage.sharedInstance + fetchAndUpdateMetricsWithHandler:^GDTCORMetricsMetadata *_Nonnull( + GDTCORMetricsMetadata *_Nullable fetchedMetadata, NSError *_Nullable fetchError) { + XCTAssertNil(fetchedMetadata); + XCTAssertNotNil(fetchError); + return [GDTCORMetricsMetadata + metadataWithCollectionStartDate:[NSDate date] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + }]; + + // Then + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(fetchAndUpdatePromise.isFulfilled); + // - Finally, there should be only one new file. + NSArray *contentsPaths = [[NSFileManager defaultManager] + contentsOfDirectoryAtPath:[GDTCORFlatFileStorage libraryDataStoragePath] + error:&error]; + XCTAssertEqual(contentsPaths.count, 1); + XCTAssertTrue([NSFileManager.defaultManager fileExistsAtPath:metricsMetadataPath]); +} + - (void)testSaveAndLoadLibraryData { __weak NSData *weakData; NSString *dataKey = NSStringFromSelector(_cmd); @@ -759,13 +906,23 @@ - (void)testCheckForExpirations_WhenEventsExpire { [self waitForExpectations:@[ expectation ] timeout:10]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:delay]]; [[GDTCORFlatFileStorage sharedInstance] checkForExpirations]; + + GDTCORMetricsControllerFake *metricsController = [[GDTCORMetricsControllerFake alloc] init]; + [GDTCORFlatFileStorage sharedInstance].delegate = metricsController; + XCTestExpectation *metricsControllerExpectation = + [self expectationWithDescription:@"metricsControllerExpectation"]; + metricsController.onStorageDidRemoveExpiredEvents = ^(NSSet *events) { + XCTAssertTrue(events.count > 0); + [metricsControllerExpectation fulfill]; + }; + expectation = [self expectationWithDescription:@"hasEvent completion called"]; [[GDTCORFlatFileStorage sharedInstance] hasEventsForTarget:kGDTCORTargetTest onComplete:^(BOOL hasEvents) { XCTAssertFalse(hasEvents); [expectation fulfill]; }]; - [self waitForExpectations:@[ expectation ] timeout:10]; + [self waitForExpectations:@[ metricsControllerExpectation, expectation ] timeout:10]; } - (void)testCheckForExpirations_WhenBatchWithNotExpiredEventsExpires { @@ -778,6 +935,17 @@ - (void)testCheckForExpirations_WhenBatchWithNotExpiredEventsExpires { // 0.2. Wait for batch expiration. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:batchExpiresIn]]; + // 0.3. Expect that the storage delegate will not be invoked because no + // events will expire in this test. + GDTCORMetricsControllerFake *metricsController = [[GDTCORMetricsControllerFake alloc] init]; + [GDTCORFlatFileStorage sharedInstance].delegate = metricsController; + XCTestExpectation *metricsControllerExpectation = + [self expectationWithDescription:@"metricsControllerExpectation"]; + metricsControllerExpectation.inverted = YES; + metricsController.onStorageDidRemoveExpiredEvents = ^(NSSet *events) { + [metricsControllerExpectation fulfill]; + }; + // 1. Check for expiration. [[GDTCORFlatFileStorage sharedInstance] checkForExpirations]; @@ -808,7 +976,10 @@ - (void)testCheckForExpirations_WhenBatchWithNotExpiredEventsExpires { XCTAssertEqualObjects(batchEventsIDs, generatedEventsIDs); }]; - [self waitForExpectations:@[ getBatchesExpectation, getEventsExpectation ] timeout:0.5]; + [self waitForExpectations:@[ + metricsControllerExpectation, getBatchesExpectation, getEventsExpectation + ] + timeout:0.5]; } - (void)testCheckForExpirations_WhenBatchWithExpiredEventsExpires { @@ -820,6 +991,17 @@ - (void)testCheckForExpirations_WhenBatchWithExpiredEventsExpires { // 0.2. Wait for batch expiration. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:batchExpiresIn]]; + // 0.3. Expect that the storage delegate will be invoked because events will + // expire in this test. + GDTCORMetricsControllerFake *metricsController = [[GDTCORMetricsControllerFake alloc] init]; + [GDTCORFlatFileStorage sharedInstance].delegate = metricsController; + XCTestExpectation *metricsControllerExpectation = + [self expectationWithDescription:@"metricsControllerExpectation"]; + metricsController.onStorageDidRemoveExpiredEvents = ^(NSSet *events) { + XCTAssertTrue(events.count > 0); + [metricsControllerExpectation fulfill]; + }; + // 1. Check for expiration. [[GDTCORFlatFileStorage sharedInstance] checkForExpirations]; @@ -847,7 +1029,10 @@ - (void)testCheckForExpirations_WhenBatchWithExpiredEventsExpires { XCTAssertEqual(batchEvents.count, 0); }]; - [self waitForExpectations:@[ getBatchesExpectation, getEventsExpectation ] timeout:0.5]; + [self waitForExpectations:@[ + metricsControllerExpectation, getBatchesExpectation, getEventsExpectation + ] + timeout:0.5]; } #pragma mark - Remove Batch tests @@ -1136,6 +1321,15 @@ - (void)testStoreEvent_WhenSizeLimitReached_ThenNewEventIsSkipped { mappingID:nil]; event.expirationDate = [NSDate dateWithTimeIntervalSinceNow:1000]; + GDTCORMetricsControllerFake *metricsControllerFake = [[GDTCORMetricsControllerFake alloc] init]; + storage.delegate = metricsControllerFake; + XCTestExpectation *metricsControllerExpectation = + [self expectationWithDescription:@"metricsControllerExpectation"]; + metricsControllerFake.onStorageDidDropEvent = ^(GDTCOREvent *droppedEvent) { + XCTAssertEqual(droppedEvent, event); + [metricsControllerExpectation fulfill]; + }; + XCTestExpectation *storeExpectation1 = [self expectationWithDescription:@"storeExpectation1"]; [storage storeEvent:event onComplete:^(BOOL wasWritten, NSError *_Nullable error) { @@ -1145,7 +1339,7 @@ - (void)testStoreEvent_WhenSizeLimitReached_ThenNewEventIsSkipped { XCTAssertEqual(error.code, GDTCORFlatFileStorageErrorSizeLimitReached); [storeExpectation1 fulfill]; }]; - [self waitForExpectations:@[ storeExpectation1 ] timeout:5]; + [self waitForExpectations:@[ metricsControllerExpectation, storeExpectation1 ] timeout:5]; // 4. Check the storage size didn't change. XCTAssertEqual([self storageSize], storageSize); @@ -1263,6 +1457,10 @@ - (void)testStoreEvent_WhenSizeLimitReached_ThenNewEventIsSkipped { [eventStoredExpectation fulfill]; }]; + dispatch_sync([GDTCORFlatFileStorage sharedInstance].storageQueue, ^{ + // Drain queue to allow event to be stored before proceeding. + }); + [self waitForExpectations:@[ eventStoredExpectation ] timeout:1]; [generatedEvents addObject:generatedEvent]; diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORLogSourceMetricsTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORLogSourceMetricsTest.m new file mode 100644 index 00000000..819ad371 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORLogSourceMetricsTest.m @@ -0,0 +1,330 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h" +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" + +#pragma mark - GDTCORLogSourceMetrics + Internal + +typedef NSDictionary GDTCORDroppedEventLogSourceMetrics; + +@interface GDTCORLogSourceMetrics (Internal) + +/// Initializer exposed for testing. +- (instancetype)initWithDroppedEventCounterByLogSource: + (NSDictionary *) + droppedEventLogSourceMetricsByMappingID; + +@end + +#pragma mark - GDTCORLogSourceMetricsTest + +@interface GDTCORLogSourceMetricsTest : GDTCORTestCase +@end + +@implementation GDTCORLogSourceMetricsTest + +- (void)testLogSourceMetricsFactoryCreatesAndReturnsLogSourceMetrics { + // Given + GDTCORLogSourceMetrics *logSourceMetrics1 = [GDTCORLogSourceMetrics metrics]; + GDTCORLogSourceMetrics *logSourceMetrics2 = [GDTCORLogSourceMetrics metrics]; + // Then + XCTAssertFalse(logSourceMetrics1 == logSourceMetrics2); +} + +- (void)testLogSourceMetricsFactoryReturnsEmptyLogSourceMetrics { + // Given + GDTCORLogSourceMetrics *logSourceMetrics = [GDTCORLogSourceMetrics metrics]; + // Then + GDTCORLogSourceMetrics *expectedLogSourceMetrics = + [[GDTCORLogSourceMetrics alloc] initWithDroppedEventCounterByLogSource:@{}]; + XCTAssertEqualObjects(logSourceMetrics, expectedLogSourceMetrics); +} + +- (void)testLogSourceMetricsWithEvents_WhenEventsAreEmpty_CreatesEmptyLogSourceMetrics { + // Given + GDTCORLogSourceMetrics *logSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:@[] droppedForReason:GDTCOREventDropReasonUnknown]; + // Then + XCTAssertEqualObjects(logSourceMetrics, [GDTCORLogSourceMetrics metrics]); +} + +- (void)testLogSourceMetricsWithEvents { + // Given + NSArray *events1 = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest] + ]; + + GDTCORLogSourceMetrics *logSourceMetrics1 = + [GDTCORLogSourceMetrics metricsWithEvents:events1 + droppedForReason:GDTCOREventDropReasonStorageFull]; + + NSArray *events2 = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics2 = + [GDTCORLogSourceMetrics metricsWithEvents:events2 + droppedForReason:GDTCOREventDropReasonServerError]; + + // When + GDTCORLogSourceMetrics *mergedLogSourceMetrics = + [logSourceMetrics1 logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics2]; + // - Assert that merging the above log source metrics in other ways result in same log source + // metrics. + [self assertMergedLogSourceMetricsFromLogSourceMetrics1:logSourceMetrics1 + logSourceMetrics2:logSourceMetrics2]; + + // Then + GDTCORLogSourceMetrics *expectedLogSourceMetrics = + [[GDTCORLogSourceMetrics alloc] initWithDroppedEventCounterByLogSource:@{ + @"log_src_1" : @{@(GDTCOREventDropReasonStorageFull) : @(2)}, + @"log_src_2" : @{ + @(GDTCOREventDropReasonStorageFull) : @(1), + @(GDTCOREventDropReasonServerError) : @(1), + }, + }]; + XCTAssertEqualObjects(mergedLogSourceMetrics, expectedLogSourceMetrics); +} + +- (void)testMergingLogSourceMetrics_WhenBothLogSourceMetricsAreEmpty_ReturnsEmptyLogSourceMetrics { + // Given + GDTCORLogSourceMetrics *logSourceMetrics1 = [GDTCORLogSourceMetrics metrics]; + GDTCORLogSourceMetrics *logSourceMetrics2 = [GDTCORLogSourceMetrics metrics]; + // Then + [self assertMergedLogSourceMetricsFromLogSourceMetrics1:logSourceMetrics1 + logSourceMetrics2:logSourceMetrics2]; +} + +- (void) + testMergingLogSourceMetrics_WhenGivenLogSourceMetricsIsEmpty_ReturnsReceivingLogSourceMetrics { + // Given + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"log_src" target:kGDTCORTargetFLL]; + GDTCORLogSourceMetrics *logSourceMetrics1 = + [GDTCORLogSourceMetrics metricsWithEvents:@[ event ] + droppedForReason:GDTCOREventDropReasonUnknown]; + GDTCORLogSourceMetrics *logSourceMetrics2 = [GDTCORLogSourceMetrics metrics]; + // When + GDTCORLogSourceMetrics *mergedLogSourceMetrics = + [logSourceMetrics1 logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics2]; + [self assertMergedLogSourceMetricsFromLogSourceMetrics1:logSourceMetrics1 + logSourceMetrics2:logSourceMetrics2]; + // Then + XCTAssertEqualObjects(mergedLogSourceMetrics, logSourceMetrics1); + XCTAssertFalse(mergedLogSourceMetrics == logSourceMetrics1); +} + +- (void)testMergingLogSourceMetrics_WhenLogSourceMetricsKeysDontCollide { + // Given + NSArray *events1 = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_3" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics1 = + [GDTCORLogSourceMetrics metricsWithEvents:events1 + droppedForReason:GDTCOREventDropReasonStorageFull]; + + NSArray *events2 = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_4" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_5" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_6" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics2 = + [GDTCORLogSourceMetrics metricsWithEvents:events2 + droppedForReason:GDTCOREventDropReasonStorageFull]; + + // When + GDTCORLogSourceMetrics *mergedLogSourceMetrics = + [logSourceMetrics1 logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics2]; + // - Assert that merging the above log source metrics in other ways result in same log source + // metrics. + [self assertMergedLogSourceMetricsFromLogSourceMetrics1:logSourceMetrics1 + logSourceMetrics2:logSourceMetrics2]; + + // Then + // - Expect no collisions for both the mapping IDs and drop reasons. + GDTCORLogSourceMetrics *expectedLogSourceMetrics = + [[GDTCORLogSourceMetrics alloc] initWithDroppedEventCounterByLogSource:@{ + @"log_src_1" : @{@(GDTCOREventDropReasonStorageFull) : @(1)}, + @"log_src_2" : @{@(GDTCOREventDropReasonStorageFull) : @(1)}, + @"log_src_3" : @{@(GDTCOREventDropReasonStorageFull) : @(1)}, + @"log_src_4" : @{@(GDTCOREventDropReasonStorageFull) : @(1)}, + @"log_src_5" : @{@(GDTCOREventDropReasonStorageFull) : @(1)}, + @"log_src_6" : @{@(GDTCOREventDropReasonStorageFull) : @(1)}, + }]; + XCTAssertEqualObjects(mergedLogSourceMetrics, expectedLogSourceMetrics); +} + +- (void)testMergingLogSourceMetrics_WhenLogSourceMetricsKeysCollide { + // Given + NSArray *events1 = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_3" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics1 = + [GDTCORLogSourceMetrics metricsWithEvents:events1 + droppedForReason:GDTCOREventDropReasonStorageFull]; + + NSArray *events2 = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_3" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics2 = + [GDTCORLogSourceMetrics metricsWithEvents:events2 + droppedForReason:GDTCOREventDropReasonStorageFull]; + + // When + GDTCORLogSourceMetrics *mergedLogSourceMetrics = + [logSourceMetrics1 logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics2]; + // - Assert that merging the above log source metrics in other ways result in same log source + // metrics. + [self assertMergedLogSourceMetricsFromLogSourceMetrics1:logSourceMetrics1 + logSourceMetrics2:logSourceMetrics2]; + + // Then + // - Expect resolved collisions for both the mapping IDs and drop reasons. + GDTCORLogSourceMetrics *expectedLogSourceMetrics = + [[GDTCORLogSourceMetrics alloc] initWithDroppedEventCounterByLogSource:@{ + @"log_src_1" : @{@(GDTCOREventDropReasonStorageFull) : @(2)}, + @"log_src_2" : @{@(GDTCOREventDropReasonStorageFull) : @(2)}, + @"log_src_3" : @{@(GDTCOREventDropReasonStorageFull) : @(2)}, + }]; + XCTAssertEqualObjects(mergedLogSourceMetrics, expectedLogSourceMetrics); +} + +- (void)testEqualityEdgeCases { + GDTCORLogSourceMetrics *logSourceMetrics1 = [GDTCORLogSourceMetrics metrics]; + GDTCORLogSourceMetrics *logSourceMetrics2 = logSourceMetrics1; + XCTAssert([logSourceMetrics1 isEqual:logSourceMetrics2]); + XCTAssertFalse([logSourceMetrics1 isEqual:@"some string"]); + XCTAssertFalse([logSourceMetrics1 isEqual:nil]); +} + +- (void)testEqualObjectsHaveSameHash { + // Given + GDTCORLogSourceMetrics *logSourceMetrics1 = + [GDTCORLogSourceMetrics metricsWithEvents:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + ] + droppedForReason:GDTCOREventDropReasonStorageFull]; + + GDTCORLogSourceMetrics *logSourceMetrics2 = + [GDTCORLogSourceMetrics metricsWithEvents:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + ] + droppedForReason:GDTCOREventDropReasonStorageFull]; + + GDTCORLogSourceMetrics *logSourceMetrics3 = + [GDTCORLogSourceMetrics metricsWithEvents:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + ] + droppedForReason:GDTCOREventDropReasonStorageFull]; + + // Then + XCTAssertEqualObjects(logSourceMetrics1, logSourceMetrics2); + XCTAssertEqual(logSourceMetrics1.hash, logSourceMetrics2.hash); + XCTAssertNotEqualObjects(logSourceMetrics2, logSourceMetrics3); + XCTAssertNotEqual(logSourceMetrics2.hash, logSourceMetrics3.hash); +} + +- (void)testSecureCoding { + // Given + NSArray *events = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:events + droppedForReason:GDTCOREventDropReasonStorageFull]; + + // When + // - Encode the log source metrics. + NSError *encodeError; + NSData *encodedLogSourceMetrics = GDTCOREncodeArchive(logSourceMetrics, nil, &encodeError); + XCTAssertNil(encodeError); + XCTAssertNotNil(encodedLogSourceMetrics); + + // Then + // - Decode the log source metrics from disk. + NSError *decodeError; + GDTCORLogSourceMetrics *decodedLogSourceMetrics = (GDTCORLogSourceMetrics *)GDTCORDecodeArchive( + GDTCORLogSourceMetrics.class, encodedLogSourceMetrics, &decodeError); + XCTAssertNil(decodeError); + XCTAssertNotNil(decodedLogSourceMetrics); + XCTAssertEqualObjects(decodedLogSourceMetrics, logSourceMetrics); +} + +- (void)testSecureCoding_WhenEncodingIsCorrupted { + // Given + // - Create an invalid instance and write its encoding to a file. When + // decoding, the invalid encoding should be treated as a corrupt encoding. + GDTCORLogSourceMetrics *corruptedMetricsLogSourceMetrics = +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincompatible-pointer-types" + [[GDTCORLogSourceMetrics alloc] initWithDroppedEventCounterByLogSource:@"corrupted"]; +#pragma clang diagnostic pop + + NSError *encodeError; + NSData *encodedMetricsLogSourceMetrics = + GDTCOREncodeArchive(corruptedMetricsLogSourceMetrics, nil, &encodeError); + XCTAssertNil(encodeError); + XCTAssertNotNil(encodedMetricsLogSourceMetrics); + + // When + NSError *decodeError; + GDTCORLogSourceMetrics *decodedMetricsLogSourceMetrics = + (GDTCORLogSourceMetrics *)GDTCORDecodeArchive(GDTCORLogSourceMetrics.class, + encodedMetricsLogSourceMetrics, &decodeError); + // Then + XCTAssertNotNil(decodeError); + XCTAssertNil(decodedMetricsLogSourceMetrics); +} + +- (void) + assertMergedLogSourceMetricsFromLogSourceMetrics1:(GDTCORLogSourceMetrics *)logSourceMetrics1 + logSourceMetrics2:(GDTCORLogSourceMetrics *)logSourceMetrics2 { + // There are three ways to merge the given log source metrics, and all three ways + // should result in the same merged log source metrics. + GDTCORLogSourceMetrics *mergedLogSourceMetrics1 = + [logSourceMetrics1 logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics2]; + + GDTCORLogSourceMetrics *mergedLogSourceMetrics2 = + [logSourceMetrics2 logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics1]; + + GDTCORLogSourceMetrics *mergedLogSourceMetrics3 = [[GDTCORLogSourceMetrics.metrics + logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics1] + logSourceMetricsByMergingWithLogSourceMetrics:logSourceMetrics2]; + + XCTAssertEqualObjects(mergedLogSourceMetrics1, mergedLogSourceMetrics2); + XCTAssertEqualObjects(mergedLogSourceMetrics2, mergedLogSourceMetrics3); +} + +@end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORMetricsControllerTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORMetricsControllerTest.m new file mode 100644 index 00000000..d16ffd7f --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORMetricsControllerTest.m @@ -0,0 +1,349 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h" + +#import "FBLPromise+Await.h" +#import "FBLPromise+Testing.h" + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORTargets.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsController.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorageMetadata.h" + +#import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h" + +@interface GDTCORMetricsControllerTest : GDTCORTestCase +@end + +@implementation GDTCORMetricsControllerTest + +- (void)testLoggingEvents_WhenEventSetIsEmpty_ThenNoOp { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + // When + [metricsController logEventsDroppedForReason:GDTCOREventDropReasonUnknown events:[NSSet set]]; + + // Then + __auto_type metricsPromise = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert([metricsPromise isRejected]); +} + +- (void)testLoggingEvents_WhenNoMetricsAreStored_StoresEventsAsMetrics { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + NSSet *droppedEvents = [NSSet setWithArray:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + ]]; + + GDTCORLogSourceMetrics *expectedLogSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:[droppedEvents allObjects] + droppedForReason:GDTCOREventDropReasonUnknown]; + + // When + [metricsController logEventsDroppedForReason:GDTCOREventDropReasonUnknown events:droppedEvents]; + + // Then + __auto_type metricsPromise = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + + XCTAssertEqualObjects([metricsPromise.value logSourceMetrics], expectedLogSourceMetrics); +} + +- (void)testLoggingEvents_WhenMetricsAreStored_StoresEventsAsMetrics { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + NSSet *droppedEvents1 = [NSSet setWithArray:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + ]]; + + [metricsController logEventsDroppedForReason:GDTCOREventDropReasonUnknown events:droppedEvents1]; + + // When + NSSet *droppedEvents2 = [NSSet setWithArray:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_3" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_3" target:kGDTCORTargetTest], + ]]; + + [metricsController logEventsDroppedForReason:GDTCOREventDropReasonUnknown events:droppedEvents2]; + + // Then + __auto_type metricsPromise = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + + GDTCORLogSourceMetrics *expectedLogSourceMetrics1 = + [GDTCORLogSourceMetrics metricsWithEvents:[droppedEvents1 allObjects] + droppedForReason:GDTCOREventDropReasonUnknown]; + + GDTCORLogSourceMetrics *expectedLogSourceMetrics2 = + [GDTCORLogSourceMetrics metricsWithEvents:[droppedEvents2 allObjects] + droppedForReason:GDTCOREventDropReasonUnknown]; + + GDTCORLogSourceMetrics *expectedLogSourceMetricsCombined = [expectedLogSourceMetrics1 + logSourceMetricsByMergingWithLogSourceMetrics:expectedLogSourceMetrics2]; + + XCTAssertEqualObjects([metricsPromise.value logSourceMetrics], expectedLogSourceMetricsCombined); +} + +- (void)testGetAndResetMetrics_WhenNoMetricsDataIsStored_ReturnsRejectedPromise { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + // When + __auto_type metricsPromise = [metricsController getAndResetMetrics]; + + // Then + // - Assert that the retrieved metrics promise was rejected. + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(metricsPromise.isRejected); + + // - Assert that metrics metadata was reset to start tracking metrics data + // from this point forward. + __auto_type metricsPromise2 = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + + // Dates should be roughly equal (within one second of each other). + XCTAssertEqualWithAccuracy(metricsPromise2.value.collectionStartDate.timeIntervalSince1970, + NSDate.date.timeIntervalSince1970, 1); + // The log source metrics should be empty. + XCTAssertEqualObjects([metricsPromise2.value logSourceMetrics], [GDTCORLogSourceMetrics metrics]); +} + +- (void)testGetAndResetMetrics_WhenMetricsDataIsStored_ReturnsMetrics { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + NSSet *droppedEvents = [NSSet setWithArray:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + ]]; + + GDTCORLogSourceMetrics *expectedLogSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:[droppedEvents allObjects] + droppedForReason:GDTCOREventDropReasonUnknown]; + + // When + [metricsController logEventsDroppedForReason:GDTCOREventDropReasonUnknown events:droppedEvents]; + + // Then + // - Assert that the retrieved metrics have the expected log source metrics. + __auto_type metricsPromise1 = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + + XCTAssertEqualObjects([metricsPromise1.value logSourceMetrics], expectedLogSourceMetrics); + + // - Assert that metrics metadata was reset to start tracking metrics data + // from this point forward. + __auto_type metricsPromise2 = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + + // Dates should be roughly equal (within one second of each other). + XCTAssertEqualWithAccuracy(metricsPromise2.value.collectionStartDate.timeIntervalSince1970, + NSDate.date.timeIntervalSince1970, 1); + // The log source metrics should be empty. + XCTAssertEqualObjects([metricsPromise2.value logSourceMetrics], [GDTCORLogSourceMetrics metrics]); +} + +- (void)testOfferMetrics_WhenStorageErrorAndOfferedMetricsAreValid_ThenAcceptMetrics { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + // - Create valid metrics to offer. + GDTCORMetricsMetadata *metricsMetadata = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantPast] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + + GDTCORStorageMetadata *storageMetadata = + [GDTCORStorageMetadata metadataWithCurrentCacheSize:15 * 1000 * 1000 // 15 MB + maxCacheSize:20 * 1000 * 1000]; // 20 MB + + GDTCORMetrics *metrics = [GDTCORMetrics metricsWithMetricsMetadata:metricsMetadata + storageMetadata:storageMetadata]; + + // When + // - Storage is empty and will pass back an error. See ``GDTCORStorageFake``. + __auto_type offerPromise = [metricsController offerMetrics:metrics]; + + // Then + // - Assert promise was resolved. + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(offerPromise.isFulfilled); + + // - Assert that retrieving the metrics contains the metadata from the offered metrics. + __auto_type metricsPromise1 = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + XCTAssertEqualObjects([metricsPromise1.value collectionStartDate], + metricsMetadata.collectionStartDate); + XCTAssertEqualObjects([metricsPromise1.value logSourceMetrics], metricsMetadata.logSourceMetrics); +} + +- (void)testOfferMetrics_WhenStorageErrorAndOfferedMetricsAreInvalid_ThenRejectMetrics { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + // - Create invalid metrics to offer. + GDTCORMetricsMetadata *metricsMetadata = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantFuture] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + + GDTCORStorageMetadata *storageMetadata = + [GDTCORStorageMetadata metadataWithCurrentCacheSize:15 * 1000 * 1000 // 15 MB + maxCacheSize:20 * 1000 * 1000]; // 20 MB + + GDTCORMetrics *metrics = [GDTCORMetrics metricsWithMetricsMetadata:metricsMetadata + storageMetadata:storageMetadata]; + + // When + // - Storage is empty and will pass back an error. See ``GDTCORStorageFake``. + __auto_type offerPromise = [metricsController offerMetrics:metrics]; + + // Then + // - Assert promise was resolved. + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(offerPromise.isFulfilled); + + // - Assert that retrieving the metrics contains the metadata from the offered metrics. + __auto_type metricsPromise = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + // Dates should be roughly equal (within one second of each other). + XCTAssertEqualWithAccuracy(metricsPromise.value.collectionStartDate.timeIntervalSince1970, + NSDate.date.timeIntervalSince1970, 1); + // The log source metrics should be empty. + XCTAssertEqualObjects([metricsPromise.value logSourceMetrics], [GDTCORLogSourceMetrics metrics]); +} + +- (void)testOfferMetrics_WhenOfferedMetricsAreInvalid_ThenRejectMetrics { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + // - Populate storage with metrics metadata from dropped events. + NSSet *droppedEvents = [NSSet setWithArray:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + ]]; + + GDTCORLogSourceMetrics *expectedLogSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:[droppedEvents allObjects] + droppedForReason:GDTCOREventDropReasonUnknown]; + + [metricsController logEventsDroppedForReason:GDTCOREventDropReasonUnknown events:droppedEvents]; + + // - Create invalid metrics to offer. + GDTCORMetricsMetadata *metricsMetadata = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantFuture] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + + GDTCORStorageMetadata *storageMetadata = + [GDTCORStorageMetadata metadataWithCurrentCacheSize:15 * 1000 * 1000 // 15 MB + maxCacheSize:20 * 1000 * 1000]; // 20 MB + + GDTCORMetrics *metrics = [GDTCORMetrics metricsWithMetricsMetadata:metricsMetadata + storageMetadata:storageMetadata]; + + // When + __auto_type offerPromise = [metricsController offerMetrics:metrics]; + + // Then + // - Assert promise was resolved. + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(offerPromise.isFulfilled); + + // - Assert that retrieving the metrics contains the metadata from the original metrics. + __auto_type metricsPromise = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + XCTAssertEqualObjects([metricsPromise.value logSourceMetrics], expectedLogSourceMetrics); +} + +- (void)testOfferMetrics_WhenOfferedMetricsAreValid_ThenAcceptMetrics { + // Given + GDTCORMetricsController *metricsController = + [[GDTCORMetricsController alloc] initWithStorage:[GDTCORStorageFake storageFake]]; + + // - Populate storage with metrics metadata from dropped events. + NSSet *droppedEvents = [NSSet setWithArray:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + ]]; + + GDTCORLogSourceMetrics *originalLogSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:[droppedEvents allObjects] + droppedForReason:GDTCOREventDropReasonUnknown]; + + [metricsController logEventsDroppedForReason:GDTCOREventDropReasonUnknown events:droppedEvents]; + + // - Create valid metrics to offer. + NSSet *droppedEventsToOffer = [NSSet setWithArray:@[ + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_source_3" target:kGDTCORTargetTest], + ]]; + + GDTCORLogSourceMetrics *counterToOffer = + [GDTCORLogSourceMetrics metricsWithEvents:[droppedEventsToOffer allObjects] + droppedForReason:GDTCOREventDropReasonUnknown]; + + GDTCORMetricsMetadata *metricsMetadata = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantPast] + logSourceMetrics:counterToOffer]; + + GDTCORStorageMetadata *storageMetadata = + [GDTCORStorageMetadata metadataWithCurrentCacheSize:15 * 1000 * 1000 // 15 MB + maxCacheSize:20 * 1000 * 1000]; // 20 MB + + GDTCORMetrics *metrics = [GDTCORMetrics metricsWithMetricsMetadata:metricsMetadata + storageMetadata:storageMetadata]; + + // When + __auto_type offerPromise = [metricsController offerMetrics:metrics]; + + // Then + // - Assert promise was resolved. + FBLWaitForPromisesWithTimeout(0.5); + XCTAssert(offerPromise.isFulfilled); + + // - Assert that retrieving the metrics contains the combined metadata from + // the original metrics and the offered metrics. + __auto_type metricsPromise = [metricsController getAndResetMetrics]; + FBLWaitForPromisesWithTimeout(0.5); + XCTAssertEqualObjects([metricsPromise.value collectionStartDate], [NSDate distantPast]); + GDTCORLogSourceMetrics *expectedCombinedLogSourceMetrics = + [originalLogSourceMetrics logSourceMetricsByMergingWithLogSourceMetrics:counterToOffer]; + XCTAssertEqualObjects([metricsPromise.value logSourceMetrics], expectedCombinedLogSourceMetrics); +} + +@end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORMetricsMetadataTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORMetricsMetadataTest.m new file mode 100644 index 00000000..40e7c2ca --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORMetricsMetadataTest.m @@ -0,0 +1,111 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h" + +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORMetricsMetadata.h" + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCOREventDropReason.h" +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" +#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORLogSourceMetrics.h" +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h" + +@interface GDTCORMetricsMetadataTest : XCTestCase + +@end + +@implementation GDTCORMetricsMetadataTest + +- (void)testEqualityEdgeCases { + GDTCORMetricsMetadata *metricsMetadata1 = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantPast] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + GDTCORMetricsMetadata *metricsMetadata2 = metricsMetadata1; + XCTAssert([metricsMetadata1 isEqual:metricsMetadata2]); + XCTAssertFalse([metricsMetadata1 isEqual:@"some string"]); + XCTAssertFalse([metricsMetadata1 isEqual:nil]); +} + +- (void)testEqualObjectsHaveSameHash { + // Given + NSArray *events = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:events + droppedForReason:GDTCOREventDropReasonStorageFull]; + + GDTCORMetricsMetadata *metricsMetadata1 = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantPast] + logSourceMetrics:logSourceMetrics]; + + GDTCORMetricsMetadata *metricsMetadata2 = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantPast] + logSourceMetrics:logSourceMetrics]; + + GDTCORMetricsMetadata *metricsMetadata3 = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate distantFuture] + logSourceMetrics:[GDTCORLogSourceMetrics metrics]]; + // Then + XCTAssertEqualObjects(metricsMetadata1, metricsMetadata2); + XCTAssertEqual(metricsMetadata1.hash, metricsMetadata2.hash); + XCTAssertNotEqualObjects(metricsMetadata2, metricsMetadata3); + XCTAssertNotEqual(metricsMetadata2.hash, metricsMetadata3.hash); +} + +- (void)testSecureCoding { + // Given + NSArray *events = @[ + [[GDTCOREvent alloc] initWithMappingID:@"log_src_1" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + [[GDTCOREvent alloc] initWithMappingID:@"log_src_2" target:kGDTCORTargetTest], + ]; + + GDTCORLogSourceMetrics *logSourceMetrics = + [GDTCORLogSourceMetrics metricsWithEvents:events + droppedForReason:GDTCOREventDropReasonStorageFull]; + + GDTCORMetricsMetadata *metricsMetadata = + [GDTCORMetricsMetadata metadataWithCollectionStartDate:[NSDate date] + logSourceMetrics:logSourceMetrics]; + + // When + // - Encode the metrics metadata. + NSError *encodeError; + NSData *encodedMetricsMetadata = GDTCOREncodeArchive(metricsMetadata, nil, &encodeError); + XCTAssertNil(encodeError); + XCTAssertNotNil(encodedMetricsMetadata); + + // - Write it to disk. + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"metadata.dat"]; + NSError *writeError; + BOOL writeResult = GDTCORWriteDataToFile(encodedMetricsMetadata, filePath, &writeError); + XCTAssertNil(writeError); + XCTAssertTrue(writeResult); + + // Then + // - Decode the metrics metadata from disk. + NSError *decodeError; + GDTCORMetricsMetadata *decodedMetricsMetadata = + (GDTCORMetricsMetadata *)GDTCORDecodeArchiveAtPath(GDTCORMetricsMetadata.class, filePath, + &decodeError); + XCTAssertNil(decodeError); + XCTAssertNotNil(decodedMetricsMetadata); + XCTAssertEqualObjects(decodedMetricsMetadata, metricsMetadata); +} + +@end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORProductDataTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORProductDataTest.m new file mode 100644 index 00000000..2fa4702a --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORProductDataTest.m @@ -0,0 +1,79 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h" + +#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORProductData.h" + +#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h" + +@interface GDTCORProductDataTest : GDTCORTestCase + +@end + +@implementation GDTCORProductDataTest + +static int32_t kTestProductID = 123456; + +- (void)testInit { + GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:kTestProductID]; + XCTAssertEqual(productData.productID, kTestProductID); +} + +- (void)testCopy { + GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:kTestProductID]; + XCTAssertEqualObjects([productData copy], productData); +} + +- (void)testIsEqualAndHash { + GDTCORProductData *productData1 = [[GDTCORProductData alloc] initWithProductID:0000]; + GDTCORProductData *productData2 = [[GDTCORProductData alloc] initWithProductID:0000]; + GDTCORProductData *productData3 = [[GDTCORProductData alloc] initWithProductID:1111]; + + XCTAssertEqualObjects(productData1, productData2); + XCTAssertNotEqualObjects(productData1, productData3); + + XCTAssertEqual(productData1.hash, productData2.hash); + XCTAssertNotEqual(productData1.hash, productData3.hash); +} + +- (void)testSecureCoding { + // Given + GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:kTestProductID]; + + // When + // - Encode. + NSError *encodeError; + NSData *encodedProductData = GDTCOREncodeArchive(productData, nil, &encodeError); + XCTAssertNil(encodeError); + XCTAssertNotNil(encodedProductData); + + // - Write to disk. + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"productdata.dat"]; + NSError *writeError; + BOOL writeResult = GDTCORWriteDataToFile(encodedProductData, filePath, &writeError); + XCTAssertNil(writeError); + XCTAssertTrue(writeResult); + + // Then + // - Decode from disk. + NSError *decodeError; + GDTCORProductData *decodedProductData = (GDTCORProductData *)GDTCORDecodeArchiveAtPath( + GDTCORProductData.class, filePath, &decodeError); + XCTAssertNil(decodeError); + XCTAssertNotNil(decodedProductData); + XCTAssertEqualObjects(decodedProductData, productData); +} + +@end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORRegistrarTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORRegistrarTest.m index fdb51dd3..464eee48 100644 --- a/GoogleDataTransport/GDTCORTests/Unit/GDTCORRegistrarTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORRegistrarTest.m @@ -19,6 +19,8 @@ #import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h" #import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h" +#import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORMetricsControllerFake.h" +#import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h" #import "GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" @interface GDTCORRegistrarTest : GDTCORTestCase @@ -47,6 +49,58 @@ - (void)testRegisterUpload { XCTAssertEqual(uploader, registrar.targetToUploader[@(_target)]); } -// TODO(mikehaney24): Add test for registering a storage. +/** Test registering a storage. */ +- (void)testRegisterStorage { + GDTCORRegistrar *registrar = [GDTCORRegistrar sharedInstance]; + GDTCORStorageFake *storage = [[GDTCORStorageFake alloc] init]; + XCTAssertNoThrow([registrar registerStorage:storage target:self.target]); + XCTAssertEqual(storage, registrar.targetToStorage[@(_target)]); +} + +/** Test registering a metrics controller. */ +- (void)testRegisterMetricsController { + GDTCORRegistrar *registrar = [GDTCORRegistrar sharedInstance]; + GDTCORMetricsControllerFake *metricsController = [[GDTCORMetricsControllerFake alloc] init]; + XCTAssertNoThrow([registrar registerMetricsController:metricsController target:self.target]); + XCTAssertEqual(metricsController, registrar.targetToMetricsController[@(_target)]); +} + +/** Tests that the metrics controller is set as the storage delegate when the storage object is + * registered first. Since objects are registered at `+ load` time, the order in which the storage + * and metrics controller objects are registered is non-deterministic. + */ +- (void)testMetricsControllerIsSetAsStorageDelegate_WhenStorageIsRegisteredFirst { + // Given + GDTCORRegistrar *registrar = [GDTCORRegistrar sharedInstance]; + // When + GDTCORStorageFake *storage = [[GDTCORStorageFake alloc] init]; + XCTAssertNoThrow([registrar registerStorage:storage target:self.target]); + GDTCORMetricsControllerFake *metricsController = [[GDTCORMetricsControllerFake alloc] init]; + XCTAssertNoThrow([registrar registerMetricsController:metricsController target:self.target]); + // Then + dispatch_sync(registrar.registrarQueue, ^{ + // Drain queue. + }); + XCTAssertEqual(storage.delegate, metricsController); +} + +/** Tests that the metrics controller is set as the storage delegate when the metrics controller + * object is registered first. Since objects are registered at `+ load` time, the order in which the + * storage and metrics controller objects are registered is non-deterministic. + */ +- (void)testMetricsControllerIsSetAsStorageDelegate_WhenMetricsControllerIsRegisteredFirst { + // Given + GDTCORRegistrar *registrar = [GDTCORRegistrar sharedInstance]; + // When + GDTCORMetricsControllerFake *metricsController = [[GDTCORMetricsControllerFake alloc] init]; + XCTAssertNoThrow([registrar registerMetricsController:metricsController target:self.target]); + GDTCORStorageFake *storage = [[GDTCORStorageFake alloc] init]; + XCTAssertNoThrow([registrar registerStorage:storage target:self.target]); + // Then + dispatch_sync(registrar.registrarQueue, ^{ + // Drain queue. + }); + XCTAssertEqual(storage.delegate, metricsController); +} @end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransformerTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransformerTest.m index 0c1bf5f5..cb23f738 100644 --- a/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransformerTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransformerTest.m @@ -37,7 +37,7 @@ @interface GDTCORTransformerTestNilingTransformer : NSObject *events = @[ + [transport eventForTransport], + [transport eventForTransportWithProductData:[[GDTCORProductData alloc] + initWithProductID:kTestProductID]] + ]; + for (GDTCOREvent *event in events) { + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; + + XCTestExpectation *writtenExpectation = [self expectationWithDescription:@"event written"]; + XCTAssertNoThrow([transport sendTelemetryEvent:event + onComplete:^(BOOL wasWritten, NSError *_Nullable error) { + XCTAssertTrue(wasWritten); + XCTAssertNil(error); + [writtenExpectation fulfill]; + }]); + [self waitForExpectations:@[ writtenExpectation ] timeout:10.0]; + } } /** Tests sending a data event. */ @@ -80,16 +122,23 @@ - (void)testSendDataEvent { transformers:nil target:kGDTCORTargetTest]; transport.transformerInstance = [[GDTCORTransformerFake alloc] init]; - GDTCOREvent *event = [transport eventForTransport]; - event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; - XCTestExpectation *writtenExpectation = [self expectationWithDescription:@"event written"]; - XCTAssertNoThrow([transport sendDataEvent:event - onComplete:^(BOOL wasWritten, NSError *_Nullable error) { - XCTAssertTrue(wasWritten); - XCTAssertNil(error); - [writtenExpectation fulfill]; - }]); - [self waitForExpectations:@[ writtenExpectation ] timeout:10.0]; + NSArray *events = @[ + [transport eventForTransport], + [transport eventForTransportWithProductData:[[GDTCORProductData alloc] + initWithProductID:kTestProductID]] + ]; + for (GDTCOREvent *event in events) { + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; + + XCTestExpectation *writtenExpectation = [self expectationWithDescription:@"event written"]; + XCTAssertNoThrow([transport sendDataEvent:event + onComplete:^(BOOL wasWritten, NSError *_Nullable error) { + XCTAssertTrue(wasWritten); + XCTAssertNil(error); + [writtenExpectation fulfill]; + }]); + [self waitForExpectations:@[ writtenExpectation ] timeout:10.0]; + } } @end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadCoordinatorTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadCoordinatorTest.m index 44df2286..f6a0f7f0 100644 --- a/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadCoordinatorTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadCoordinatorTest.m @@ -24,7 +24,7 @@ #import "GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h" #import "GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h" -#import "GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h" +#import "GoogleDataTransport/GDTCCTTests/Common/TestStorage/GDTCCTTestStorage.h" #import "GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h" #import "GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" @@ -32,7 +32,7 @@ @interface GDTCORUploadCoordinatorTest : GDTCORTestCase /** A storage fake to inject into GDTCORUploadCoordinator. */ -@property(nonatomic) GDTCORStorageFake *storageFake; +@property(nonatomic) GDTCCTTestStorage *storageFake; /** A test uploader. */ @property(nonatomic) GDTCORTestUploader *uploader; @@ -43,7 +43,7 @@ @implementation GDTCORUploadCoordinatorTest - (void)setUp { [super setUp]; - self.storageFake = [[GDTCORStorageFake alloc] init]; + self.storageFake = [[GDTCCTTestStorage alloc] init]; self.uploader = [[GDTCORTestUploader alloc] init]; [[GDTCORRegistrar sharedInstance] registerUploader:_uploader target:kGDTCORTargetTest]; @@ -112,6 +112,8 @@ - (void)testThatAFailedUploadResultsInAnEventualRetry { enumerateObjectsUsingBlock:^(GDTCOREvent *_Nonnull obj, BOOL *_Nonnull stop) { [self.storageFake storeEvent:obj onComplete:nil]; }]; + + XCTestExpectation *eventsBatched = [self expectationWithDescription:@"Events batched"]; __block NSNumber *batchID; [storage batchWithEventSelector:[GDTCORStorageEventSelector eventSelectorForTarget:kGDTCORTargetTest] @@ -119,7 +121,10 @@ - (void)testThatAFailedUploadResultsInAnEventualRetry { onComplete:^(NSNumber *_Nullable newBatchID, NSSet *_Nullable events) { batchID = newBatchID; + [eventsBatched fulfill]; }]; + + [self waitForExpectations:@[ eventsBatched ] timeout:0.5]; self.uploader.uploadWithConditionsBlock = ^(GDTCORTarget target, GDTCORUploadConditions conditions) { [storage removeBatchWithID:batchID deleteEvents:NO onComplete:nil]; diff --git a/GoogleDataTransport/GDTTestApp/EventCleanupPerfTest.swift b/GoogleDataTransport/GDTTestApp/EventCleanupPerfTest.swift index b0bc831d..d8164bf8 100644 --- a/GoogleDataTransport/GDTTestApp/EventCleanupPerfTest.swift +++ b/GoogleDataTransport/GDTTestApp/EventCleanupPerfTest.swift @@ -20,9 +20,9 @@ import GoogleDataTransport import os.signpost -/// The test actions to run under the profiler to measure performance of `GDTCORFlatFileStorage.checkForExpirations()` method. -@available(iOS 12.0, *) -class EventCleanupPerfTest { +/// The test actions to run under the profiler to measure performance of +/// `GDTCORFlatFileStorage.checkForExpirations()` method. +enum EventCleanupPerfTest { static let log = OSLog(subsystem: "GoogleDataTransport-TestApp", category: "EventCleanupPerfTest") static func run(completion: @escaping () -> Void) { @@ -44,7 +44,7 @@ class EventCleanupPerfTest { os_signpost(.begin, log: log, name: "generateTestEvents", signpostID: signpostID) - _ = (0 ..< count).compactMap { (_) -> GDTCOREvent? in + _ = (0 ..< count).compactMap { _ -> GDTCOREvent? in group.enter() let event = GDTCOREventGenerator.generateEvent(for: .test, qosTier: nil, mappingID: nil) GDTCORFlatFileStorage.sharedInstance().store(event) { _, _ in diff --git a/GoogleDataTransport/GDTTestApp/app.swift b/GoogleDataTransport/GDTTestApp/app.swift index eae281b9..b12865ee 100644 --- a/GoogleDataTransport/GDTTestApp/app.swift +++ b/GoogleDataTransport/GDTTestApp/app.swift @@ -33,8 +33,11 @@ import GoogleDataTransport } public class ViewController: UIViewController { - let transport: GDTCORTransport = GDTCORTransport(mappingID: "1234", transformers: nil, - target: GDTCORTarget.test)! + let transport = GDTCORTransport( + mappingID: "1234", + transformers: nil, + target: GDTCORTarget.test + )! @IBOutlet var statusLabel: UILabel! } @@ -49,7 +52,10 @@ import GoogleDataTransport } public class ViewController: NSViewController { - let transport: GDTCORTransport = GDTCORTransport(mappingID: "1234", transformers: nil, - target: GDTCORTarget.test)! + let transport = GDTCORTransport( + mappingID: "1234", + transformers: nil, + target: GDTCORTarget.test + )! } #endif diff --git a/GoogleDataTransport/GDTTestApp/globals.swift b/GoogleDataTransport/GDTTestApp/globals.swift index caccc8ea..1c35a355 100644 --- a/GoogleDataTransport/GDTTestApp/globals.swift +++ b/GoogleDataTransport/GDTTestApp/globals.swift @@ -16,7 +16,7 @@ import Foundation -public struct Globals { +public enum Globals { public static var SharedViewController: ViewController? public static let MonkeyTestLength: TimeInterval = 20.0 diff --git a/GoogleDataTransport/GDTTestApp/viewcontroller.swift b/GoogleDataTransport/GDTTestApp/viewcontroller.swift index d9d9ea5d..df65d89f 100644 --- a/GoogleDataTransport/GDTTestApp/viewcontroller.swift +++ b/GoogleDataTransport/GDTTestApp/viewcontroller.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import Foundation import Dispatch +import Foundation import GoogleDataTransport public extension ViewController { @@ -64,28 +64,20 @@ public extension ViewController { } @IBAction func generateTestEvents(button: UIButton) { - if #available(iOS 12.0, *) { - let numberOfEvents = 10000 - set(status: "Generating \(numberOfEvents) events") + let numberOfEvents = 10000 + set(status: "Generating \(numberOfEvents) events") - EventCleanupPerfTest.generateTestEvents(count: numberOfEvents) { - self.set(status: "Generated \(numberOfEvents) events") - } - } else { - print("Performance testing set up for iOS 12.0 and later") + EventCleanupPerfTest.generateTestEvents(count: numberOfEvents) { + self.set(status: "Generated \(numberOfEvents) events") } } @IBAction func runEventCleanupPerformanceTest(button: UIButton) { - if #available(iOS 12.0, *) { - set(status: "Event Cleanup Performance Test started") - EventCleanupPerfTest.run { - DispatchQueue.main.async { - self.set(status: "Event Cleanup Performance Test finished") - } + set(status: "Event Cleanup Performance Test started") + EventCleanupPerfTest.run { + DispatchQueue.main.async { + self.set(status: "Event Cleanup Performance Test finished") } - } else { - print("Performance testing set up for iOS 12.0 and later") } } @@ -93,7 +85,7 @@ public extension ViewController { print("Beginning monkey test") let generateEventsQueue = DispatchQueue(label: "com.google.GDTTestApp.vc.generateEvents") - let sema: DispatchSemaphore = DispatchSemaphore(value: 0) + let sema = DispatchSemaphore(value: 0) var generateEvents = true DispatchQueue.global().asyncAfter(deadline: .now() + Globals.MonkeyTestLength) { generateEventsQueue.sync { @@ -111,11 +103,11 @@ public extension ViewController { self.generateWifiOnlyEvent, self.generateDailyEvent, ] - let randomIndex: Int = Int.random(in: 0 ..< generationFunctions.count) + let randomIndex: Int = .random(in: 0 ..< generationFunctions.count) generationFunctions[randomIndex](self) } RunLoop.current.run(until: Date(timeIntervalSinceNow: Double.random(in: 0 ..< 1.5))) - var shouldContinueGeneratingEvents: Bool = false + var shouldContinueGeneratingEvents = false generateEventsQueue.sync { shouldContinueGeneratingEvents = generateEvents } diff --git a/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/InterfaceController.swift b/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/InterfaceController.swift index 06cffb6c..c9ed9d65 100644 --- a/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/InterfaceController.swift +++ b/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/InterfaceController.swift @@ -14,15 +14,14 @@ * limitations under the License. */ -import WatchKit -import Foundation import Dispatch +import Foundation +import WatchKit import GoogleDataTransport class InterfaceController: WKInterfaceController { - let transport: GDTCORTransport = GDTCORTransport(mappingID: "1234", transformers: nil, - target: GDTCORTarget.test)! + let transport = GDTCORTransport(mappingID: "1234", transformers: nil, target: GDTCORTarget.test)! override func awake(withContext context: Any?) { super.awake(withContext: context) diff --git a/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/NotificationController.swift b/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/NotificationController.swift index 03387acb..a57ecd16 100644 --- a/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/NotificationController.swift +++ b/GoogleDataTransport/GDTWatchOSTestApp/GDTWatchOSTestAppWatchKitExtension/NotificationController.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import WatchKit import Foundation import UserNotifications +import WatchKit class NotificationController: WKUserNotificationInterfaceController { override init() { diff --git a/GoogleDataTransport/GDTWatchOSTestApp/Podfile b/GoogleDataTransport/GDTWatchOSTestApp/Podfile index 54ddf699..d7d49663 100644 --- a/GoogleDataTransport/GDTWatchOSTestApp/Podfile +++ b/GoogleDataTransport/GDTWatchOSTestApp/Podfile @@ -1,3 +1,4 @@ +source 'https://github.com/firebase/SpecsDev.git' source 'https://github.com/firebase/SpecsStaging.git' source 'https://cdn.cocoapods.org/' diff --git a/GoogleDataTransport/ProtoSupport/Protos/cct.options b/GoogleDataTransport/ProtoSupport/Protos/cct.options index c3989642..88ccdfac 100644 --- a/GoogleDataTransport/ProtoSupport/Protos/cct.options +++ b/GoogleDataTransport/ProtoSupport/Protos/cct.options @@ -23,6 +23,11 @@ gdt_cct.IosClientInfo.model type:FT_POINTER gdt_cct.IosClientInfo.language_code type:FT_POINTER gdt_cct.IosClientInfo.application_bundle_id type:FT_POINTER +gdt_cct.MacClientInfo.os_major_version type:FT_POINTER +gdt_cct.MacClientInfo.os_full_version type:FT_POINTER +gdt_cct.MacClientInfo.application_build type:FT_POINTER +gdt_cct.MacClientInfo.application_bundle_id type:FT_POINTER + gdt_cct.LogRequest.log_event type:FT_POINTER gdt_cct.BatchedLogRequest.log_request type:FT_POINTER diff --git a/GoogleDataTransport/ProtoSupport/Protos/cct.proto b/GoogleDataTransport/ProtoSupport/Protos/cct.proto index 8b32dced..8d644269 100644 --- a/GoogleDataTransport/ProtoSupport/Protos/cct.proto +++ b/GoogleDataTransport/ProtoSupport/Protos/cct.proto @@ -18,6 +18,8 @@ syntax = "proto2"; package gdt_cct; +import "compliance.proto"; + message LogEvent { optional int64 event_time_ms = 1; @@ -30,6 +32,8 @@ message LogEvent { optional sint64 timezone_offset_seconds = 15; optional NetworkConnectionInfo network_connection_info = 23; + + optional ComplianceData compliance_data = 33; } message NetworkConnectionInfo { @@ -88,6 +92,21 @@ message NetworkConnectionInfo { optional MobileSubtype mobile_subtype = 2 [default = UNKNOWN_MOBILE_SUBTYPE]; } +message MacClientInfo { + + // The major OS version of the macOS client. Eg: "10.7". + optional string os_major_version = 1; + + // The complete OS version of a macOS client. Eg: "10.7.3". + optional string os_full_version = 2; + + // The client application version. + optional string application_build = 3; + + // The bundle name of the application. + optional string application_bundle_id = 7; +} + message IosClientInfo { // The major OS version of an iOS client. Eg: "8", "9". @@ -131,6 +150,8 @@ message ClientInfo { optional ClientType client_type = 1; optional IosClientInfo ios_client_info = 4; + + optional MacClientInfo mac_client_info = 13; } message BatchedLogRequest { diff --git a/GoogleDataTransport/ProtoSupport/Protos/client_metrics.options b/GoogleDataTransport/ProtoSupport/Protos/client_metrics.options new file mode 100644 index 00000000..0e104680 --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/client_metrics.options @@ -0,0 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Options for client_metrics.proto + +gdt_client_metrics.ClientMetrics.log_source_metrics type:FT_POINTER +gdt_client_metrics.ClientMetrics.app_namespace type:FT_POINTER + +gdt_client_metrics.LogSourceMetrics.log_source type:FT_POINTER +gdt_client_metrics.LogSourceMetrics.log_event_dropped type:FT_POINTER \ No newline at end of file diff --git a/GoogleDataTransport/ProtoSupport/Protos/client_metrics.proto b/GoogleDataTransport/ProtoSupport/Protos/client_metrics.proto new file mode 100644 index 00000000..a859f7cc --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/client_metrics.proto @@ -0,0 +1,92 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package gdt_client_metrics; + +// Top level metrics for all client analytics metrics. +// These metrics should be sent as a part of every request that is uploaded to +// FireLog server. In more detail, an additional LogRequest should be added to +// the BatchedLogRequest, where the LogSource of the LogRequest should be +// GDT_CLIENT_METRICS and the LogRequest should have a single LogEvent whose +// payload is a GoogleDataTransportClientMetrics message. +// +// See go/firelog-client-analytics for more details. +message ClientMetrics { + // The window of time over which the metrics are evaluated. + TimeWindow window = 1; + + repeated LogSourceMetrics log_source_metrics = 2; + + GlobalMetrics global_metrics = 3; + + // The bundle ID on Apple platforms (e.g., iOS) or the package name on Android + string app_namespace = 4; +} + +// Represents an arbitrary window of time. +message TimeWindow { + // The time that the window first starts. + // start_ms is the number of milliseconds since the UNIX epoch + // (January 1, 1970 00:00:00 UTC) + int64 start_ms = 1; + + // The time that the window ends. + // end_ms is the number of milliseconds since the UNIX epoch + // (January 1, 1970 00:00:00 UTC) + int64 end_ms = 2; +} + +// Metrics per app, not per log source +message GlobalMetrics { + StorageMetrics storage_metrics = 1; +} + +message StorageMetrics { + // The number of bytes of storage the event cache was consuming on the client + // at the time the request was sent. + int64 current_cache_size_bytes = 1; + + // The maximum number of bytes to which the event cache is allowed to grow. + int64 max_cache_size_bytes = 2; +} + +// Metrics per log source. +message LogSourceMetrics { + // A LogSource uniquely identifies a logging configuration. log_source should + // contains a string value of the LogSource from + // google3/wireless/android/play/playlog/proto/clientanalytics.proto + string log_source = 1; + + repeated LogEventDropped log_event_dropped = 2; +} + +message LogEventDropped { + // A count of how many log event have been dropped on the client. + int64 events_dropped_count = 1; + + // The reason why log events have been dropped on the client. + enum Reason { + REASON_UNKNOWN = 0; + MESSAGE_TOO_OLD = 1; + CACHE_FULL = 2; + PAYLOAD_TOO_BIG = 3; + MAX_RETRIES_REACHED = 4; + INVALID_PAYLOD = 5; + SERVER_ERROR = 6; + } + + Reason reason = 3; +} diff --git a/GoogleDataTransport/ProtoSupport/Protos/compliance.options b/GoogleDataTransport/ProtoSupport/Protos/compliance.options new file mode 100644 index 00000000..ecb0dbaa --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/compliance.options @@ -0,0 +1,15 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Options for compliance.proto diff --git a/GoogleDataTransport/ProtoSupport/Protos/compliance.proto b/GoogleDataTransport/ProtoSupport/Protos/compliance.proto new file mode 100644 index 00000000..c5fae8f6 --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/compliance.proto @@ -0,0 +1,30 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package gdt_cct; + +import "external_privacy_context.proto"; + +message ComplianceData { + optional privacy.context.external.ExternalPrivacyContext privacy_context = 1; + + enum ProductIdOrigin { + NOT_SET = 0; + EVENT_OVERRIDE = 5; + } + + optional ProductIdOrigin product_id_origin = 2 [default = NOT_SET]; +} diff --git a/GoogleDataTransport/ProtoSupport/Protos/external_prequest_context.options b/GoogleDataTransport/ProtoSupport/Protos/external_prequest_context.options new file mode 100644 index 00000000..7a4cff62 --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/external_prequest_context.options @@ -0,0 +1,15 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Options for external_prequest_context.proto diff --git a/GoogleDataTransport/ProtoSupport/Protos/external_prequest_context.proto b/GoogleDataTransport/ProtoSupport/Protos/external_prequest_context.proto new file mode 100644 index 00000000..11a5b50e --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/external_prequest_context.proto @@ -0,0 +1,21 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package privacy.context.external; + +message ExternalPRequestContext { + optional int32 origin_associated_product_id = 13; +} diff --git a/GoogleDataTransport/ProtoSupport/Protos/external_privacy_context.options b/GoogleDataTransport/ProtoSupport/Protos/external_privacy_context.options new file mode 100644 index 00000000..74960867 --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/external_privacy_context.options @@ -0,0 +1,15 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Options for external_privacy_context.proto diff --git a/GoogleDataTransport/ProtoSupport/Protos/external_privacy_context.proto b/GoogleDataTransport/ProtoSupport/Protos/external_privacy_context.proto new file mode 100644 index 00000000..ba0dbf21 --- /dev/null +++ b/GoogleDataTransport/ProtoSupport/Protos/external_privacy_context.proto @@ -0,0 +1,23 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package privacy.context.external; + +import "external_prequest_context.proto"; + +message ExternalPrivacyContext { + optional ExternalPRequestContext prequest = 2; +} diff --git a/GoogleDataTransport/ProtoSupport/generate_cct_protos.sh b/GoogleDataTransport/ProtoSupport/generate_cct_protos.sh index 27880b5e..4e2ba4a4 100755 --- a/GoogleDataTransport/ProtoSupport/generate_cct_protos.sh +++ b/GoogleDataTransport/ProtoSupport/generate_cct_protos.sh @@ -22,8 +22,8 @@ readonly DIR="$( git rev-parse --show-toplevel )" -# Current release of nanopb being used to build the CCT protos -readonly NANOPB_VERSION="0.3.9.7" +# Current release of nanopb being used to build the CCT protos +readonly NANOPB_VERSION="0.3.9.9" readonly NANOPB_TEMPDIR="${DIR}/GoogleDataTransport/nanopb_temp" readonly LIBRARY_DIR="${DIR}/GoogleDataTransport/GDTCCTLibrary/" diff --git a/GoogleDataTransport/ProtoSupport/proto_generator.py b/GoogleDataTransport/ProtoSupport/proto_generator.py index 93371613..8a73a2b2 100755 --- a/GoogleDataTransport/ProtoSupport/proto_generator.py +++ b/GoogleDataTransport/ProtoSupport/proto_generator.py @@ -39,7 +39,7 @@ COPYRIGHT_NOTICE = ''' /* - * Copyright 2019 Google + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/GoogleDataTransport/Resources/PrivacyInfo.xcprivacy b/GoogleDataTransport/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..1e83fa67 --- /dev/null +++ b/GoogleDataTransport/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,30 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + + + + diff --git a/GoogleDataTransport/generate_project.sh b/GoogleDataTransport/generate_project.sh index 9d31e7dc..63bd6a3a 100755 --- a/GoogleDataTransport/generate_project.sh +++ b/GoogleDataTransport/generate_project.sh @@ -31,4 +31,4 @@ readonly DIR="$(git rev-parse --show-toplevel)" "$DIR/GoogleDataTransport/ProtoSupport/generate_cct_protos.sh" || echo "Something went wrong generating protos."; -pod gen "$DIR/GoogleDataTransport.podspec" --auto-open --local-sources=./ --gen-directory="$DIR/gen" --platforms=${platform} --clean +GDT_TEST=1 pod gen "$DIR/GoogleDataTransport.podspec" --auto-open --local-sources=./ --gen-directory="$DIR/gen" --platforms=${platform} --clean diff --git a/Mintfile b/Mintfile index 30fa82e4..640a5ca2 100644 --- a/Mintfile +++ b/Mintfile @@ -1 +1 @@ -nicklockwood/SwiftFormat@0.45.5 +nicklockwood/SwiftFormat@0.54.0 diff --git a/Package.swift b/Package.swift index 957c8fee..d49077b3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. // Copyright 2021 Google LLC @@ -19,7 +19,7 @@ import PackageDescription let package = Package( name: "GoogleDataTransport", - platforms: [.iOS(.v10), .macOS(.v10_12), .tvOS(.v10), .watchOS(.v6)], + platforms: [.iOS(.v12), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v7)], products: [ .library( name: "GoogleDataTransport", @@ -28,14 +28,12 @@ let package = Package( ], dependencies: [ .package( - name: "nanopb", url: "https://github.com/firebase/nanopb.git", - "2.30907.0" ..< "2.30908.0" + "2.30910.0" ..< "2.30911.0" ), .package( - name: "GoogleUtilities", - url: "https://github.com/google/GoogleUtilities.git", - "7.2.1" ..< "8.0.0" + url: "https://github.com/google/promises.git", + "2.4.0" ..< "3.0.0" ), ], // TODO: Restructure directory structure to simplify the excludes here. @@ -44,12 +42,10 @@ let package = Package( name: "GoogleDataTransport", dependencies: [ .product(name: "nanopb", package: "nanopb"), - .product(name: "GULEnvironment", package: "GoogleUtilities"), + .product(name: "FBLPromises", package: "Promises"), ], path: "GoogleDataTransport", exclude: [ - "CHANGELOG.md", - "README.md", "generate_project.sh", "GDTCCTWatchOSTestApp/", "GDTWatchOSTestApp/", @@ -63,6 +59,7 @@ let package = Package( "GDTCORLibrary", "GDTCCTLibrary", ], + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "GDTCORLibrary/Public", cSettings: [ .headerSearchPath("../"), @@ -72,8 +69,11 @@ let package = Package( .define("PB_ENABLE_MALLOC", to: "1"), ], linkerSettings: [ - .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS, .tvOS])), - .linkedFramework("CoreTelephony", .when(platforms: [.macOS, .iOS])), + .linkedFramework( + "SystemConfiguration", + .when(platforms: [.iOS, .macOS, .tvOS, .macCatalyst]) + ), + .linkedFramework("CoreTelephony", .when(platforms: [.macOS, .iOS, .macCatalyst])), ] ), .testTarget( diff --git a/README.md b/README.md index 2a8625db..067dafa8 100644 --- a/README.md +++ b/README.md @@ -14,42 +14,139 @@ These instructions apply to minor and patch version updates. Major versions need a customized adaptation. After the CI is green: - * Update the version in the podspec - * Add the CocoaPods tag - * `git tag CocoaPods-{version}` - * `git push origin CocoaPods-{version}` - * Push the podspec to SpecsStaging - * `pod repo push staging GoogleDataTransport.podspec` - * Run Firebase CI by waiting until next nightly or adding a PR that touches `Gemfile` - * On google3, copybara and run a global TAP which should kick off automatically after each PR. +* Determine the next version for release by checking the + [tagged releases](https://github.com/google/GoogleDataTransport/tags). + Ensure that the next release version keeps the Swift PM and CocoaPods versions in sync. +* Verify that the releasing version is the latest entry in the [CHANGELOG.md](CHANGELOG.md), + updating it if necessary. +* Update the version in the podspec to match the latest entry in the [CHANGELOG.md](CHANGELOG.md) +* Checkout the `main` branch and ensure it is up to date. + ```console + git checkout main + git pull + ``` +* Add the CocoaPods tag (`{version}` will be the latest version in the [podspec](GoogleDataTransport.podspec#L3)) + ```console + git tag CocoaPods-{version} + git push origin CocoaPods-{version} + ``` +* Push the podspec to the designated repo + * If this version of GDT is intended to launch **before or with** the next Firebase release: +
+ Push to SpecsStaging + + ```console + pod repo push --skip-tests --use-json staging GoogleDataTransport.podspec + ``` + + If the command fails with `Unable to find the 'staging' repo.`, add the staging repo with: + ```console + pod repo add staging git@github.com:firebase/SpecsStaging.git + ``` +
+ * Otherwise: +
+ Push to SpecsDev + + ```console + pod repo push --skip-tests --use-json dev GoogleDataTransport.podspec + ``` + + If the command fails with `Unable to find the 'dev' repo.`, add the dev repo with: + ```console + pod repo add dev git@github.com:firebase/SpecsDev.git + ``` +
+* Run Firebase CI by waiting until next nightly or adding a PR that touches `Gemfile`. +* On google3, create a workspace and new CL. Then copybara and run a global TAP. +
+  /google/data/ro/teams/copybara/copybara third_party/firebase/ios/Releases/GoogleDataTransport/copy.bara.sky \
+  --piper-description-behavior=OVERWRITE \
+  --destination-cl=YOUR_CL gdt
+  
## Publishing +The release process is as follows: +1. [Tag and release for Swift PM](#swift-package-manager) +2. [Publish to CocoaPods](#cocoapods) +3. [Create GitHub Release](#create-github-release) +4. [Perform post release cleanup](#post-release-cleanup) + +### Swift Package Manager + By creating and [pushing a tag](https://github.com/google/GoogleDataTransport/tags) + for Swift PM, the newly tagged version will be immediately released for public use. + Given this, please verify the intended time of release for Swift PM. * Add a version tag for Swift PM - * `git tag {version}` - * `git push origin {version}` - * `pod trunk push GoogleDataTransport.podspec` - * Clean up SpecsStaging + ```console + git tag {version} + git push origin {version} + ``` + *Note: Ensure that any inflight PRs that depend on the new `GoogleDataTransport` version are updated to point to the + newly tagged version rather than a checksum.* + +### CocoaPods +* Publish the newly versioned pod to CocoaPods + + It's recommended to point to the `GoogleDataTransport.podspec` in `staging` to make sure the correct spec is being published. + + > [!WARNING] + > Manually update your local SpecsStaging clone or run `pod repo update` + > before proceeding. + + ```console + pod trunk push ~/.cocoapods/repos/staging/GoogleDataTransport/{version}/GoogleDataTransport.podspec.json --skip-tests + ``` + + The pod push was successful if the above command logs: `🚀 GoogleDataTransport ({version}) successfully published`. + In addition, a new commit that publishes the new version (co-authored by [CocoaPodsAtGoogle](https://github.com/CocoaPodsAtGoogle)) + should appear in the [CocoaPods specs repo](https://github.com/CocoaPods/Specs). Last, the latest version should be displayed + on [GoogleDataTransport's CocoaPods page](https://cocoapods.org/pods/GoogleDataTransport). + +### [Create GitHub Release](https://github.com/google/GoogleDataTransport/releases/new/) + Update the [release template](https://github.com/google/GoogleDataTransport/releases/new/)'s **Tag version** and **Release title** + fields with the latest version. In addition, reference the [Release Notes](./CHANGELOG.md) in the release's description. + + See [this release](https://github.com/google/GoogleDataTransport/releases/edit/9.0.1) for an example. + + *Don't forget to perform the [post release cleanup](#post-release-cleanup)!* + +### Post Release Cleanup +
+ Clean up SpecsStaging + + ```console + pwd=$(pwd) + mkdir -p /tmp/release-cleanup && cd $_ + git clone git@github.com:firebase/SpecsStaging.git + cd SpecsStaging/ + git rm -rf GoogleDataTransport/ + git commit -m "Post publish cleanup" + git push origin master + rm -rf /tmp/release-cleanup + cd $pwd + ``` +
## Set logging level ### Swift - Import `GoogleDataTransport` module: - ``` + ```swift import GoogleDataTransport ``` -- Set logging level global variable to the desired value before calling `FirebaseApp.config()`: - ``` +- Set logging level global variable to the desired value before calling `FirebaseApp.configure()`: + ```swift GDTCORConsoleLoggerLoggingLevel = GDTCORLoggingLevel.debug.rawValue ``` ### Objective-C - Import `GoogleDataTransport`: - ``` + ```objective-c #import ``` -- Set logging level global variable to the desired value before calling `-[FIRApp config]`: - ``` +- Set logging level global variable to the desired value before calling `-[FIRApp configure]`: + ```objective-c GDTCORConsoleLoggerLoggingLevel = GDTCORLoggingLevelDebug; ``` @@ -61,7 +158,7 @@ After the CI is green: ## To develop -- Run `generate_project.sh` after installing the prereqs +- Run `./GoogleDataTransport/generate_project.sh` after installing the prereqs ## When adding new logging endpoint @@ -109,17 +206,7 @@ Alternatively disable signing in each target: ### Code Formatting -To ensure that the code is formatted consistently, run the script -[./scripts/check.sh](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/check.sh) -before creating a PR. - -GitHub Actions will verify that any code changes are done in a style compliant -way. Install `clang-format` and `mint`: - -``` -brew install clang-format@11 -brew install mint -``` +See [firebase-ios-sdk's code formatting docs][firebase-ios-sdk-formatting]. ### Running Unit Tests @@ -137,3 +224,4 @@ The contents of this repository is licensed under the [gh-actions]: https://github.com/firebase/firebase-ios-sdk/actions [gh-datatransport-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/datatransport/badge.svg +[firebase-ios-sdk-formatting]: https://github.com/firebase/firebase-ios-sdk?tab=readme-ov-file#code-formatting diff --git a/setup-scripts.sh b/setup-scripts.sh index 423b82ad..f4480149 100755 --- a/setup-scripts.sh +++ b/setup-scripts.sh @@ -17,6 +17,8 @@ git clone \ https://github.com/firebase/firebase-ios-sdk.git \ ; cd firebase-ios-sdk -git checkout master -- scripts +git checkout main -- scripts cd .. ln -s firebase-ios-sdk/scripts scripts + +gem install xcpretty