Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 3469 dependency transition 11 #3562

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions Sources/App/Core/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ struct AppEnvironment: Sendable {
var logger: @Sendable () -> Logger
var metricsPushGatewayUrl: @Sendable () -> String?
var plausibleBackendReportingSiteID: @Sendable () -> String?
var postPlausibleEvent: @Sendable (Client, Plausible.Event.Kind, Plausible.Path, User?) async throws -> Void
var processingBuildBacklog: @Sendable () -> Bool
var runnerIds: @Sendable () -> [String]
var setHTTPClient: @Sendable (Client) -> Void
Expand All @@ -65,19 +64,6 @@ struct AppEnvironment: Sendable {
}


extension AppEnvironment {
func postPlausibleEvent(_ event: Plausible.Event.Kind, path: Plausible.Path, user: User?) {
Task {
do {
try await Current.postPlausibleEvent(Current.httpClient(), event, path, user)
} catch {
Current.logger().warning("Plausible.postEvent failed: \(error)")
}
}
}
}


extension AppEnvironment {
nonisolated(unsafe) static var httpClient: Client!
nonisolated(unsafe) static var logger: Logger!
Expand Down Expand Up @@ -115,7 +101,6 @@ extension AppEnvironment {
logger: { logger },
metricsPushGatewayUrl: { Environment.get("METRICS_PUSHGATEWAY_URL") },
plausibleBackendReportingSiteID: { Environment.get("PLAUSIBLE_BACKEND_REPORTING_SITE_ID") },
postPlausibleEvent: { client, kind, path, user in try await Plausible.postEvent(client: client, kind: kind, path: path, user: user) },
processingBuildBacklog: {
Environment.get("PROCESSING_BUILD_BACKLOG").flatMap(\.asBool) ?? false
},
Expand Down
4 changes: 3 additions & 1 deletion Sources/App/Core/BackendReportingMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Dependencies
import Vapor


Expand All @@ -23,7 +24,8 @@ struct BackendReportingMiddleware: AsyncMiddleware {
let response = try await next.respond(to: request)
guard isActive else { return response }
let user = try? request.auth.require(User.self)
Current.postPlausibleEvent(.pageview, path: path, user: user)
@Dependency(\.httpClient) var httpClient
try await httpClient.postPlausibleEvent(kind: .pageview, path: path, user: user)
return response
}
}
5 changes: 3 additions & 2 deletions Sources/App/Core/Dependencies/EnvironmentClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
struct EnvironmentClient {
// See https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependenciesmacros/dependencyclient()#Restrictions
// regarding the use of XCTFail here.
// Closures returning optionals or Void don't need this, because they automatically get the default failing
// Closures that are throwing or return Void don't need this, because they automatically get the default failing
// mechanism when they're not set up in a test.
var allowBuildTriggers: @Sendable () -> Bool = { XCTFail("allowBuildTriggers"); return true }
var allowSocialPosts: @Sendable () -> Bool = { XCTFail("allowSocialPosts"); return true }
Expand All @@ -42,7 +42,8 @@
var currentReferenceCache: @Sendable () -> CurrentReferenceCache?
var dbId: @Sendable () -> String?
var mastodonCredentials: @Sendable () -> Mastodon.Credentials?
var mastodonPost: @Sendable (_ client: Client, _ post: String) async throws -> Void
#warning("drop client parameter and move this to httpClient")

Check warning on line 45 in Sources/App/Core/Dependencies/EnvironmentClient.swift

View workflow job for this annotation

GitHub Actions / Query Performance Test

drop client parameter and move this to httpClient

Check warning on line 45 in Sources/App/Core/Dependencies/EnvironmentClient.swift

View workflow job for this annotation

GitHub Actions / Query Performance Test

drop client parameter and move this to httpClient

Check warning on line 45 in Sources/App/Core/Dependencies/EnvironmentClient.swift

View workflow job for this annotation

GitHub Actions / Query Performance Test

drop client parameter and move this to httpClient

Check warning on line 45 in Sources/App/Core/Dependencies/EnvironmentClient.swift

View workflow job for this annotation

GitHub Actions / Test

drop client parameter and move this to httpClient

Check warning on line 45 in Sources/App/Core/Dependencies/EnvironmentClient.swift

View workflow job for this annotation

GitHub Actions / Test

drop client parameter and move this to httpClient

Check warning on line 45 in Sources/App/Core/Dependencies/EnvironmentClient.swift

View workflow job for this annotation

GitHub Actions / Test

drop client parameter and move this to httpClient

Check warning on line 45 in Sources/App/Core/Dependencies/EnvironmentClient.swift

View workflow job for this annotation

GitHub Actions / Test

drop client parameter and move this to httpClient
var mastodonPost: @Sendable (_ client: Client, _ message: String) async throws -> Void
var random: @Sendable (_ range: ClosedRange<Double>) -> Double = { XCTFail("random"); return Double.random(in: $0) }

enum FailureMode: String {
Expand Down
14 changes: 14 additions & 0 deletions Sources/App/Core/Dependencies/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,22 @@ import Vapor

@DependencyClient
struct HTTPClient {
typealias Request = Vapor.HTTPClient.Request
typealias Response = Vapor.HTTPClient.Response

var post: @Sendable (_ url: String, _ headers: HTTPHeaders, _ body: Data) async throws -> Response
var fetchDocumentation: @Sendable (_ url: URI) async throws -> Response
var fetchHTTPStatusCode: @Sendable (_ url: String) async throws -> HTTPStatus
var postPlausibleEvent: @Sendable (_ kind: Plausible.Event.Kind, _ path: Plausible.Path, _ user: User?) async throws -> Void
}

extension HTTPClient: DependencyKey {
static var liveValue: HTTPClient {
.init(
post: { url, headers, body in
let req = try Request(url: url, method: .POST, headers: headers, body: .data(body))
return try await Vapor.HTTPClient.shared.execute(request: req).get()
},
fetchDocumentation: { url in
try await Vapor.HTTPClient.shared.get(url: url.string).get()
},
Expand All @@ -45,6 +52,9 @@ extension HTTPClient: DependencyKey {
} defer: {
try await client.shutdown()
}
},
postPlausibleEvent: { kind, path, user in
try await Plausible.postEvent(kind: kind, path: path, user: user)
}
)
}
Expand Down Expand Up @@ -74,6 +84,10 @@ extension HTTPClient {
.init(status: .ok, headers: headers, body: .init(string: url.path))
}
}

static var noop: @Sendable (_ kind: Plausible.Event.Kind, _ path: Plausible.Path, _ user: User?) async throws -> Void {
{ _, _, _ in }
}
}

extension HTTPClient.Response {
Expand Down
17 changes: 9 additions & 8 deletions Sources/App/Core/Plausible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import Vapor
import Dependencies


enum Plausible {
Expand Down Expand Up @@ -43,18 +44,18 @@ enum Plausible {
var message: String
}

static let postEventURI = URI(string: "https://plausible.io/api/event")
static let postEventURL = "https://plausible.io/api/event"

static func postEvent(client: Client, kind: Event.Kind, path: Path, user: User?) async throws {
static func postEvent(kind: Event.Kind, path: Path, user: User?) async throws {
guard let siteID = Current.plausibleBackendReportingSiteID() else {
throw Error(message: "PLAUSIBLE_BACKEND_REPORTING_SITE_ID not set")
}
let res = try await client.post(postEventURI, headers: .applicationJSON) { req in
try req.content.encode(Event(name: .pageview,
url: "https://\(siteID)\(path.rawValue)",
domain: siteID,
props: user.props))
}
let body = try JSONEncoder().encode(Event(name: .pageview,
url: "https://\(siteID)\(path.rawValue)",
domain: siteID,
props: user.props))
@Dependency(\.httpClient) var httpClient
let res = try await httpClient.post(url: postEventURL, headers: .applicationJSON, body: body)
guard res.status.succeeded else {
throw Error(message: "Request failed with status code: \(res.status)")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Core/Social.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ enum Social {
throw Error.invalidMessage
}
// Ignore errors from here for now to keep concurrency simpler
async let _ = try? await environment.mastodonPost(client, message)
async let _ = try? await environment.mastodonPost(client: client, message: message)
}

static func postToFirehose(client: Client,
Expand Down
36 changes: 15 additions & 21 deletions Tests/AppTests/ApiTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ApiTests: AppTestCase {
func test_search_noQuery() throws {
try withDependencies {
$0.environment.apiSigningKey = { "secret" }
$0.httpClient.postPlausibleEvent = App.HTTPClient.noop
} operation: {
// MUT
try app.test(.GET, "api/search",
Expand All @@ -48,8 +49,12 @@ class ApiTests: AppTestCase {
}

func test_search_basic_param() async throws {
let event = App.ActorIsolated<TestEvent?>(nil)
try await withDependencies {
$0.environment.apiSigningKey = { "secret" }
$0.httpClient.postPlausibleEvent = { @Sendable kind, path, _ in
await event.setValue(.init(kind: kind, path: path))
}
} operation: {
let p1 = Package(id: .id0, url: "1")
try await p1.save(on: app.db)
Expand All @@ -69,11 +74,6 @@ class ApiTests: AppTestCase {
try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db)
try await Search.refresh(on: app.db)

let event = App.ActorIsolated<TestEvent?>(nil)
Current.postPlausibleEvent = { @Sendable _, kind, path, _ in
await event.setValue(.init(kind: kind, path: path))
}

// MUT
try await app.test(.GET, "api/search?query=foo%20bar",
headers: .bearerApplicationJSON(try .apiToken(secretKey: "secret", tier: .tier1)),
Expand Down Expand Up @@ -765,6 +765,7 @@ class ApiTests: AppTestCase {
}

func test_get_badge() async throws {
// sas 2024-12-20: Badges are not reporting plausbile events, because they triggered way too many events. (This is an old changes, just adding this comment today as I'm removing the old, commented out test remnants we still had in place.)
// setup
let owner = "owner"
let repo = "repo"
Expand All @@ -782,11 +783,6 @@ class ApiTests: AppTestCase {
try await Build(version: v, platform: .macosXcodebuild, status: .ok, swiftVersion: .v1)
.save(on: app.db)

let event = App.ActorIsolated<TestEvent?>(nil)
Current.postPlausibleEvent = { @Sendable _, kind, path, _ in
await event.setValue(.init(kind: kind, path: path))
}

// MUT - swift versions
try await app.test(
.GET,
Expand Down Expand Up @@ -822,21 +818,20 @@ class ApiTests: AppTestCase {
XCTAssertEqual(badge.cacheSeconds, 6*3600)
XCTAssertNotNil(badge.logoSvg)
})

// ensure API event has been reported
// API reporting for badges is currently disabled, because it is very noisy
// await event.withValue {
// XCTAssertEqual($0, .some(.init(kind: .pageview, path: .badge)))
// }
}

func test_package_collections_owner() async throws {
try XCTSkipIf(!isRunningInCI && EnvironmentClient.liveValue.collectionSigningPrivateKey() == nil, "Skip test for local user due to unset COLLECTION_SIGNING_PRIVATE_KEY env variable")

let event = App.ActorIsolated<TestEvent?>(nil)
try await withDependencies {
$0.date.now = .t0
$0.environment.apiSigningKey = { "secret" }
$0.environment.collectionSigningCertificateChain = EnvironmentClient.liveValue.collectionSigningCertificateChain
$0.environment.collectionSigningPrivateKey = EnvironmentClient.liveValue.collectionSigningPrivateKey
$0.httpClient.postPlausibleEvent = { @Sendable kind, path, _ in
await event.setValue(.init(kind: kind, path: path))
}
} operation: {
// setup
let p1 = Package(id: .id1, url: "1")
Expand All @@ -855,11 +850,6 @@ class ApiTests: AppTestCase {
try await Product(version: v, type: .library(.automatic), name: "lib")
.save(on: app.db)

let event = App.ActorIsolated<TestEvent?>(nil)
Current.postPlausibleEvent = { @Sendable _, kind, path, _ in
await event.setValue(.init(kind: kind, path: path))
}

do { // MUT
let body: ByteBuffer = .init(string: """
{
Expand Down Expand Up @@ -901,12 +891,14 @@ class ApiTests: AppTestCase {

func test_package_collections_packageURLs() async throws {
try XCTSkipIf(!isRunningInCI && EnvironmentClient.liveValue.collectionSigningPrivateKey() == nil, "Skip test for local user due to unset COLLECTION_SIGNING_PRIVATE_KEY env variable")

let refDate = Date(timeIntervalSince1970: 0)
try await withDependencies {
$0.date.now = refDate
$0.environment.apiSigningKey = { "secret" }
$0.environment.collectionSigningCertificateChain = EnvironmentClient.liveValue.collectionSigningCertificateChain
$0.environment.collectionSigningPrivateKey = EnvironmentClient.liveValue.collectionSigningPrivateKey
$0.httpClient.postPlausibleEvent = App.HTTPClient.noop
} operation: {
// setup
let p1 = Package(id: UUID(uuidString: "442cf59f-0135-4d08-be00-bc9a7cebabd3")!,
Expand Down Expand Up @@ -1049,6 +1041,7 @@ class ApiTests: AppTestCase {
try await withDependencies {
$0.environment.apiSigningKey = { "secret" }
$0.environment.dbId = { nil }
$0.httpClient.postPlausibleEvent = App.HTTPClient.noop
} operation: {
let owner = "owner"
let repo = "repo"
Expand Down Expand Up @@ -1112,6 +1105,7 @@ class ApiTests: AppTestCase {
func test_dependencies_get() async throws {
try await withDependencies {
$0.environment.apiSigningKey = { "secret" }
$0.httpClient.postPlausibleEvent = App.HTTPClient.noop
} operation: {
let pkg = try await savePackage(on: app.db, id: .id0, "http://github.com/foo/bar")
try await Repository(package: pkg,
Expand Down
1 change: 0 additions & 1 deletion Tests/AppTests/Mocks/AppEnvironment+mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ extension AppEnvironment {
logger: { logger },
metricsPushGatewayUrl: { "http://pushgateway:9091" },
plausibleBackendReportingSiteID: { nil },
postPlausibleEvent: { _, _, _, _ in },
processingBuildBacklog: { false },
runnerIds: { [] },
setHTTPClient: { client in Self.httpClient = client },
Expand Down
70 changes: 39 additions & 31 deletions Tests/AppTests/PlausibleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import XCTest

@testable import App

import Dependencies


final class PlausibleTests: XCTestCase {

Expand All @@ -29,43 +31,49 @@ final class PlausibleTests: XCTestCase {
}

func test_postEvent_anonymous() async throws {
Current.plausibleBackendReportingSiteID = { "foo.bar" }

var called = false
let client = MockClient { req, _ in
called = true
// validate
XCTAssertEqual(try? req.content.decode(Plausible.Event.self),
.init(name: .pageview,
url: "https://foo.bar/api/search",
domain: "foo.bar",
props: ["user": "none"]))
}
let called = ActorIsolated(false)
try await withDependencies {
$0.httpClient.post = { @Sendable _, _, body in
await called.withValue { $0 = true }
// validate
XCTAssertEqual(try? JSONDecoder().decode(Plausible.Event.self, from: body),
.init(name: .pageview,
url: "https://foo.bar/api/search",
domain: "foo.bar",
props: ["user": "none"]))
return .ok
}
} operation: {
Current.plausibleBackendReportingSiteID = { "foo.bar" }

// MUT
_ = try await Plausible.postEvent(client: client, kind: .pageview, path: .search, user: nil)
// MUT
_ = try await Plausible.postEvent(kind: .pageview, path: .search, user: nil)

XCTAssertTrue(called)
await called.withValue { XCTAssertTrue($0) }
}
}

func test_postEvent_package() async throws {
Current.plausibleBackendReportingSiteID = { "foo.bar" }
let called = ActorIsolated(false)
try await withDependencies {
$0.httpClient.post = { @Sendable _, _, body in
await called.withValue { $0 = true }
// validate
XCTAssertEqual(try? JSONDecoder().decode(Plausible.Event.self, from: body),
.init(name: .pageview,
url: "https://foo.bar/api/packages/{owner}/{repository}",
domain: "foo.bar",
props: ["user": "3c469e9d"]))
return .ok
}
} operation: {
Current.plausibleBackendReportingSiteID = { "foo.bar" }
let user = User(name: "api", identifier: "3c469e9d")

let user = User(name: "api", identifier: "3c469e9d")
var called = false
let client = MockClient { req, _ in
called = true
// validate
XCTAssertEqual(try? req.content.decode(Plausible.Event.self),
.init(name: .pageview,
url: "https://foo.bar/api/packages/{owner}/{repository}",
domain: "foo.bar",
props: ["user": user.identifier]))
}

// MUT
_ = try await Plausible.postEvent(client: client, kind: .pageview, path: .package, user: user)
// MUT
_ = try await Plausible.postEvent(kind: .pageview, path: .package, user: user)

XCTAssertTrue(called)
await called.withValue { XCTAssertTrue($0) }
}
}
}
Loading
Loading