diff --git a/.clomonitor.yml b/.clomonitor.yml new file mode 100644 index 000000000..20e8ff5de --- /dev/null +++ b/.clomonitor.yml @@ -0,0 +1,4 @@ +# see https://github.com/cncf/clomonitor/blob/main/docs/checks.md#exemptions +exemptions: + - check: artifacthub_badge + reason: "Artifact Hub doesn't support swift packages" diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 246c4572c..6d369906d 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -4,6 +4,9 @@ "config:best-practices", "helpers:pinGitHubActionDigestsToSemver" ], + "ignoreDeps": [ + "open-telemetry/opentelemetry-swift-core" + ], "packageRules": [ { "groupName": "all patch versions", diff --git a/.github/workflows/BuildAndTest.yml b/.github/workflows/BuildAndTest.yml index bcceff967..b7e9b0af8 100644 --- a/.github/workflows/BuildAndTest.yml +++ b/.github/workflows/BuildAndTest.yml @@ -9,26 +9,44 @@ permissions: contents: read jobs: + should-run: + runs-on: ubuntu-latest + outputs: + should-run: ${{ steps.check.outputs.should-run }} + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + fetch-depth: 0 + - name: Check if the workflow should run + id: check + run: | + git diff --name-only origin/${{ github.base_ref }} HEAD | grep -q "Sources/" && echo "should-run=true" >> "$GITHUB_OUTPUT" || echo "should-run=false" >> "$GITHUB_OUTPUT" FormattingLint: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: macos-15 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: SwiftFormat run: echo swiftformat --lint `git diff --name-only HEAD^1 HEAD` --reporter github-actions-log SwiftLint: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: GitHub Action for SwiftLint (Only files changed in the PR) uses: norio-nomura/action-swiftlint@9f4dcd7fd46b4e75d7935cf2f4df406d5cae3684 # 3.2.1 env: args: --strict DIFF_BASE: ${{ github.base_ref }} macOS: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: macos-15 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: xcode-version: 16.4 @@ -41,9 +59,11 @@ jobs: xcrun llvm-cov export -ignore-filename-regex="pb\.swift|grpc\.swift" -format="lcov" .build/debug/opentelemetry-swiftPackageTests.xctest/Contents/MacOS/opentelemetry-swiftPackageTests -instr-profile .build/debug/codecov/default.profdata > .build/debug/codecov/coverage_report.lcov ./codecov -f .build/debug/codecov/coverage_report.lcov iOS: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: macos-15 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: xcode-version: 16.4 @@ -54,9 +74,11 @@ jobs: - name: Test for iOS run: make test-without-building-ios tvOS: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: macos-15 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: xcode-version: 16.4 @@ -67,9 +89,11 @@ jobs: - name: Test for tvOS run: make test-without-building-tvos watchOS: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: macos-15 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: xcode-version: 16.4 @@ -80,9 +104,11 @@ jobs: - name: Test for watchOS run: make test-without-building-watchos visionOS: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: macos-15 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: xcode-version: 16.4 @@ -93,11 +119,41 @@ jobs: - name: Test for visionOS run: make test-without-building-visionos linux: + needs: should-run + if: ${{ needs.should-run.outputs.should-run == 'true' }} runs-on: ubuntu-latest - container: swift:6.2@sha256:1e73c4051f095f7f1bafbece9ca7f9c67de4c870246c20bf12a06c69c52dd827 + container: swift:6.2@sha256:0e4716bd34384d22963a63afbdbc93be3129dfd0753185aa1ded27755abdcae8 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Build tests for Linux run: swift build --build-tests - name: Run tests for Linux run: swift test + required-status-checks: + needs: + - FormattingLint + - SwiftLint + - macOS + - iOS + - tvOS + - watchOS + - visionOS + - linux + runs-on: ubuntu-latest + if: always() + steps: + - name: Check if all required jobs passed + run: | + if [[ ${{ needs.SwiftLint.result }} == 'failure' || \ + ${{ needs.FormattingLint.result }} == 'failure' || \ + ${{ needs.macOS.result }} == 'failure' || \ + ${{ needs.iOS.result }} == 'failure' || \ + ${{ needs.tvOS.result }} == 'failure' || \ + ${{ needs.watchOS.result }} == 'failure' || \ + ${{ needs.visionOS.result }} == 'failure' || \ + ${{ needs.linux.result }} == 'failure' ]]; then + echo "One or more required jobs failed. Failing the workflow." + exit 1 + else + echo "All required jobs passed/skipped." + fi diff --git a/.github/workflows/CodeQL-Analysis.yml b/.github/workflows/CodeQL-Analysis.yml index 8da5bd699..29ffc3561 100644 --- a/.github/workflows/CodeQL-Analysis.yml +++ b/.github/workflows/CodeQL-Analysis.yml @@ -20,10 +20,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: languages: swift queries: security-and-quality @@ -33,6 +33,6 @@ jobs: run: swift build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: category: "/language:swift" diff --git a/.github/workflows/Create-Release-PR.yml b/.github/workflows/Create-Release-PR.yml index 2ae38baf4..114d343f8 100644 --- a/.github/workflows/Create-Release-PR.yml +++ b/.github/workflows/Create-Release-PR.yml @@ -13,7 +13,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: ref: ${{ github.head_ref }} - name: update Podspec @@ -26,14 +26,14 @@ jobs: sed -i -e 's/spec.version = ".*"/spec.version = "${{ inputs.new_version }}"/' OpenTelemetry-Swift-SdkResourceExtension.podspec sed -i -e 's/spec.version = ".*"/spec.version = "${{ inputs.new_version }}"/' OpenTelemetry-Swift-PersistenceExporter.podspec - - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 id: otelbot-token with: app-id: ${{ vars.OTELBOT_APP_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows token: ${{ steps.otelbot-token.outputs.token }} diff --git a/.github/workflows/Tag-And-Release.yml b/.github/workflows/Tag-And-Release.yml index f903016d7..dde360c96 100644 --- a/.github/workflows/Tag-And-Release.yml +++ b/.github/workflows/Tag-And-Release.yml @@ -29,7 +29,7 @@ jobs: run: | version=$(echo "${{ github.event.pull_request.head.ref }}" | sed 's/^release\///') echo "version=$version" >> $GITHUB_OUTPUT - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: '0' @@ -60,7 +60,7 @@ jobs: needs: tag runs-on: macos-15 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Publish to CocoaPods trunk env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index f86b5a993..d0af31d30 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -12,7 +12,7 @@ jobs: fossa: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0 with: diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 2ae954836..1710a7465 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -19,7 +19,7 @@ jobs: # Needed for GitHub OIDC token if publish_results is true id-token: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false @@ -42,6 +42,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: sarif_file: results.sarif diff --git a/.github/workflows/update-core-dependencies.yml b/.github/workflows/update-core-dependencies.yml index cc224ba90..31b7ac6ec 100644 --- a/.github/workflows/update-core-dependencies.yml +++ b/.github/workflows/update-core-dependencies.yml @@ -23,7 +23,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Validate version format run: | @@ -45,7 +45,7 @@ jobs: echo "has_changes=false" >> $GITHUB_OUTPUT fi - - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 id: otelbot-token with: app-id: ${{ vars.OTELBOT_APP_ID }} @@ -53,7 +53,7 @@ jobs: - name: Create Pull Request if: steps.changes.outputs.has_changes == 'true' && github.event.inputs.create_pr == 'true' - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows token: ${{ steps.otelbot-token.outputs.token }} diff --git a/.gitignore b/.gitignore index 7a0e73cce..6fc8b8f89 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ playground.xcworkspace # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. Packages/ Package.pins -#Package.resolved +Package.resolved *.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata @@ -94,4 +94,4 @@ fastlane/test_output iOSInjectionProject/ -.DS_Store \ No newline at end of file +.DS_Store diff --git a/Examples/OTLP Exporter/docker-compose.yaml b/Examples/OTLP Exporter/docker-compose.yaml index 4d9906c22..e18e09c62 100644 --- a/Examples/OTLP Exporter/docker-compose.yaml +++ b/Examples/OTLP Exporter/docker-compose.yaml @@ -2,7 +2,7 @@ version: "3" services: # Collector collector: - image: otel/opentelemetry-collector:0.138.0@sha256:56951db9579bf00d3f32a4e934e19548183a86c14640798502bcd4c225976ea6 + image: otel/opentelemetry-collector:0.140.1@sha256:e448b3c73de52e379d85875c3441faf499e470ef91e775439e7937bca67e9c4f # The latest image of the otel-collector may not work, so specifying the version that works with this release # image: otel/opentelemetry-collector:latest command: ["--config=/conf/collector-config.yaml"] diff --git a/Examples/OTLP HTTP Exporter/docker-compose.yaml b/Examples/OTLP HTTP Exporter/docker-compose.yaml index 7100c8434..fa3aa9511 100644 --- a/Examples/OTLP HTTP Exporter/docker-compose.yaml +++ b/Examples/OTLP HTTP Exporter/docker-compose.yaml @@ -2,7 +2,7 @@ version: "3" services: # Collector collector: - image: otel/opentelemetry-collector:latest@sha256:8ac5df2a931e9264667b236d65bf7591fa4ba633a7a634e6caa2f0a4fc549c07 + image: otel/opentelemetry-collector:latest@sha256:6852803128c97a37fd2bafb989a04a10e3af920737d8eee998eefdfcebc698be # The latest image of the otel-collector may not work, so specifying the version that works with this release # image: otel/opentelemetry-collector:latest command: ["--config=/conf/collector-config.yaml"] diff --git a/Package.resolved b/Package.resolved index 96116a88a..fb42981f0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "c6fe6442e6a64250495669325044052e113e990c", - "version" : "1.32.0" + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", + "version" : "1.33.3" } }, { diff --git a/Package.swift b/Package.swift index 8f12103b5..882ef0847 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/open-telemetry/opentelemetry-swift-core.git", from: "2.2.0"), - .package(url: "https://github.com/apple/swift-nio.git", from: "2.87.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.90.1"), .package(url: "https://github.com/grpc/grpc-swift.git", exact: "1.27.0"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.33.3"), .package(url: "https://github.com/apple/swift-log.git", from: "1.6.4"), diff --git a/Sources/Instrumentation/NetworkStatus/NetworkStatus.swift b/Sources/Instrumentation/NetworkStatus/NetworkStatus.swift index a471b7787..e4b9c1026 100644 --- a/Sources/Instrumentation/NetworkStatus/NetworkStatus.swift +++ b/Sources/Instrumentation/NetworkStatus/NetworkStatus.swift @@ -27,7 +27,7 @@ case .cellular: if #available(iOS 13.0, *) { if let serviceId = networkInfo.dataServiceIdentifier, let value = networkInfo.serviceCurrentRadioAccessTechnology?[serviceId] { - return ("cell", simpleConnectionName(connectionType: value), networkInfo.serviceSubscriberCellularProviders?[networkInfo.dataServiceIdentifier!]) + return ("cell", simpleConnectionName(connectionType: value), networkInfo.serviceSubscriberCellularProviders?[serviceId]) } } else { if let radioType = networkInfo.currentRadioAccessTechnology { diff --git a/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift b/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift index 5f5af94ba..c6d4cea35 100644 --- a/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift +++ b/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift @@ -31,7 +31,7 @@ public class URLSessionInstrumentation { private var _configuration: URLSessionInstrumentationConfiguration public var configuration: URLSessionInstrumentationConfiguration { - get{ + get { configurationQueue.sync { _configuration } } set { @@ -87,7 +87,9 @@ public class URLSessionInstrumentation { URLSessionDataDelegate.urlSession(_:dataTask:didBecome:) as (URLSessionDataDelegate) -> ( (URLSession, URLSessionDataTask, URLSessionStreamTask) -> Void - )?) + )?), + #selector( + URLSessionTaskDelegate.urlSession(_:task:didFinishCollecting:)) ] let classes = configuration.delegateClassesToInstrument @@ -452,17 +454,6 @@ public class URLSessionInstrumentation { methodsToSwizzle.append(method) } - if NSClassFromString("AFURLSessionManager") != nil { - let classes = InstrumentationUtils.objc_getSafeClassList( - ignoredPrefixes: configuration.ignoredClassPrefixes - ) - classes.forEach { - if let method = class_getInstanceMethod($0, NSSelectorFromString("af_resume")) { - methodsToSwizzle.append(method) - } - } - } - methodsToSwizzle.forEach { let theMethod = $0 diff --git a/Sources/Instrumentation/URLSession/URLSessionInstrumentationConfiguration.swift b/Sources/Instrumentation/URLSession/URLSessionInstrumentationConfiguration.swift index 362f22214..4c0d267b9 100644 --- a/Sources/Instrumentation/URLSession/URLSessionInstrumentationConfiguration.swift +++ b/Sources/Instrumentation/URLSession/URLSessionInstrumentationConfiguration.swift @@ -40,7 +40,7 @@ public struct URLSessionInstrumentationConfiguration { self.delegateClassesToInstrument = delegateClassesToInstrument self.baggageProvider = baggageProvider self.tracer = tracer ?? - OpenTelemetry.instance.tracerProvider.get(instrumentationName: "NSURLSession", instrumentationVersion: "0.0.1") + OpenTelemetry.instance.tracerProvider.get(instrumentationName: "NSURLSession", instrumentationVersion: "1.0.0") self.ignoredClassPrefixes = ignoredClassPrefixes } @@ -92,7 +92,7 @@ public struct URLSessionInstrumentationConfiguration { /// Note: The injected baggage depends on the propagator in use (e.g., W3C or custom). /// Returns: A `Baggage` instance or `nil` if no baggage is needed. public let baggageProvider: ((inout URLRequest, Span?) -> (Baggage)?)? - + /// The Array of Prefixes you can avoid in swizzle process public let ignoredClassPrefixes: [String]? } diff --git a/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift b/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift index 6bb07a4ed..1112d66ed 100644 --- a/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift +++ b/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift @@ -53,6 +53,17 @@ class URLSessionInstrumentationTests: XCTestCase { } } + /// A minimal delegate that only implements didFinishCollecting. + /// This tests that delegate classes are discovered even when they only implement + /// urlSession(_:task:didFinishCollecting:) and no other delegate methods. + class MinimalMetricsDelegate: NSObject, URLSessionTaskDelegate { + var didFinishCollectingCalled = false + + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + didFinishCollectingCalled = true + } + } + static var requestCopy: URLRequest! static var responseCopy: HTTPURLResponse! @@ -915,15 +926,15 @@ class URLSessionInstrumentationTests: XCTestCase { public func testAsyncAwaitUploadMethodsAreNotInstrumented() async throws { let url = URL(string: "http://localhost:33333/success")! let request = URLRequest(url: url) - + // Test upload(for:from:) method let (data, response) = try await URLSession.shared.upload(for: request, from: Data()) - + guard let httpResponse = response as? HTTPURLResponse else { XCTFail("Response should be HTTPURLResponse") return } - + XCTAssertEqual(httpResponse.statusCode, 200, "Request should succeed") XCTAssertNotNil(data, "Should receive response data") @@ -931,4 +942,25 @@ class URLSessionInstrumentationTests: XCTestCase { XCTAssertTrue(URLSessionInstrumentationTests.checker.createdRequestCalled, "createdRequest should be called") XCTAssertTrue(URLSessionInstrumentationTests.checker.receivedResponseCalled, "receivedResponse should be called") } + + /// Tests that delegate classes are discovered and swizzled even when they only implement + /// urlSession(_:task:didFinishCollecting:) and no other delegate methods. + /// This is a regression test for a bug where the selector for didFinishCollecting was missing + /// from the delegate discovery mechanism. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) + public func testDelegateWithOnlyDidFinishCollectingIsDiscovered() async throws { + let request = URLRequest(url: URL(string: "http://localhost:33333/success")!) + + let delegate = MinimalMetricsDelegate() + let session = URLSession(configuration: URLSessionConfiguration.default, delegate: delegate, delegateQueue: nil) + + _ = try await session.data(for: request) + + // Verify the user's delegate was called (proves swizzling forwarded the call) + XCTAssertTrue(delegate.didFinishCollectingCalled, "Delegate should receive metrics callback") + + // Verify instrumentation worked (span was created and completed) + XCTAssertTrue(URLSessionInstrumentationTests.checker.createdRequestCalled, "Instrumentation should capture the request") + XCTAssertTrue(URLSessionInstrumentationTests.checker.receivedResponseCalled, "Instrumentation should capture the response") + } }