Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- appstoreconnect-swift-sdk updated to 4.5.1 (App Store Connect API 4.5 and 4.5.1).
- `asc screenshot-sets`, `screenshots`, `app-preview-sets` and `app-previews` say in `--help` that Apple deprecated them; they work as before. → [docs](docs/features/asset-library/README.md)
- `asc perf-metrics`, `asc diagnostics` and `asc diagnostic-logs` are now also available over REST (`asc web-server`), with `_links` to the next call. → [docs](docs/features/performance/README.md)
- asc is now licensed under Apache 2.0 instead of MIT. It stays free and open source, and the license now also grants patent rights. → [LICENSE](LICENSE)

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,9 @@ struct DiagnosticLogsList: AsyncParsableCommand {
print(try await execute(repo: repo))
}

func execute(repo: any DiagnosticsRepository) async throws -> String {
func execute(repo: any DiagnosticsRepository, affordanceMode: AffordanceMode = .cli) async throws -> String {
let logs = try await repo.listLogs(signatureId: signatureId)
let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
return try formatter.formatAgentItems(
logs,
headers: ["ID", "Bundle ID", "Version", "OS", "Device", "Event"],
rowMapper: {
[
$0.id,
$0.bundleId ?? "-",
$0.appVersion ?? "-",
$0.osVersion ?? "-",
$0.deviceType ?? "-",
$0.event ?? "-",
]
}
)
return try formatter.formatAgentItems(logs, affordanceMode: affordanceMode)
}
}
16 changes: 2 additions & 14 deletions Sources/ASCCommand/Commands/Performance/DiagnosticsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,10 @@ struct DiagnosticsList: AsyncParsableCommand {
print(try await execute(repo: repo))
}

func execute(repo: any DiagnosticsRepository) async throws -> String {
func execute(repo: any DiagnosticsRepository, affordanceMode: AffordanceMode = .cli) async throws -> String {
let filter = diagnosticType.flatMap { DiagnosticType(rawValue: $0) }
let signatures = try await repo.listSignatures(buildId: buildId, diagnosticType: filter)
let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
return try formatter.formatAgentItems(
signatures,
headers: ["ID", "Type", "Signature", "Weight", "Trend"],
rowMapper: {
[
$0.id,
$0.diagnosticType.rawValue,
$0.signature,
String($0.weight),
$0.insightDirection ?? "-",
]
}
)
return try formatter.formatAgentItems(signatures, affordanceMode: affordanceMode)
}
}
17 changes: 2 additions & 15 deletions Sources/ASCCommand/Commands/Performance/PerfMetricsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct PerfMetricsList: AsyncParsableCommand {
print(try await execute(repo: repo))
}

func execute(repo: any PerfMetricsRepository) async throws -> String {
func execute(repo: any PerfMetricsRepository, affordanceMode: AffordanceMode = .cli) async throws -> String {
let filter = metricType.flatMap { PerformanceMetricCategory(rawValue: $0) }
let metrics: [PerformanceMetric]

Expand All @@ -45,19 +45,6 @@ struct PerfMetricsList: AsyncParsableCommand {
}

let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
return try formatter.formatAgentItems(
metrics,
headers: ["ID", "Category", "Metric", "Value", "Unit", "Goal"],
rowMapper: { m in
[
m.id,
m.category.rawValue,
m.metricIdentifier,
m.latestValue.map { String($0) } ?? "-",
m.unit ?? "-",
m.goalValue.map { String($0) } ?? "-",
]
}
)
return try formatter.formatAgentItems(metrics, affordanceMode: affordanceMode)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation
import Domain
import Hummingbird
import HummingbirdWebSocket
import Infrastructure

/// `GET /builds/:buildId/diagnostics?diagnostic-type=…` — a build's diagnostic signatures;
/// `GET /diagnostics/:signatureId/logs` — the logs behind one signature.
/// The query param matches the CLI flag `--diagnostic-type`.
struct DiagnosticsController: Sendable {
let repo: any DiagnosticsRepository

func addRoutes(to group: RouterGroup<BasicWebSocketRequestContext>) {
group.get("/builds/:buildId/diagnostics") { request, context -> Response in
guard let buildId = context.parameters.get("buildId") else { return jsonError("Missing buildId") }
let diagnosticType = request.uri.queryParameters.get("diagnostic-type")
.flatMap { DiagnosticType(rawValue: String($0)) }
let signatures = try await self.repo.listSignatures(buildId: buildId, diagnosticType: diagnosticType)
return try restFormat(signatures)
}

group.get("/diagnostics/:signatureId/logs") { _, context -> Response in
guard let signatureId = context.parameters.get("signatureId") else { return jsonError("Missing signatureId") }
let logs = try await self.repo.listLogs(signatureId: signatureId)
return try restFormat(logs)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation
import Domain
import Hummingbird
import HummingbirdWebSocket
import Infrastructure

/// `GET /apps/:appId/perf-metrics` and `GET /builds/:buildId/perf-metrics` — power and
/// performance metrics. The `metric-type` query param matches the CLI flag `--metric-type`.
struct PerfMetricsController: Sendable {
let repo: any PerfMetricsRepository

func addRoutes(to group: RouterGroup<BasicWebSocketRequestContext>) {
group.get("/apps/:appId/perf-metrics") { request, context -> Response in
guard let appId = context.parameters.get("appId") else { return jsonError("Missing appId") }
let metrics = try await self.repo.listAppMetrics(appId: appId, metricType: Self.metricType(request))
return try restFormat(metrics)
}

group.get("/builds/:buildId/perf-metrics") { request, context -> Response in
guard let buildId = context.parameters.get("buildId") else { return jsonError("Missing buildId") }
let metrics = try await self.repo.listBuildMetrics(buildId: buildId, metricType: Self.metricType(request))
return try restFormat(metrics)
}
}

private static func metricType(_ request: Request) -> PerformanceMetricCategory? {
request.uri.queryParameters.get("metric-type").flatMap { PerformanceMetricCategory(rawValue: String($0)) }
}
}
6 changes: 6 additions & 0 deletions Sources/ASCCommand/Commands/Web/RESTRoutes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ enum RESTRoutes {
if let perfOverviewRepo = try? factory.makePerfOverviewRepository(authProvider: auth) {
PerfOverviewController(repo: perfOverviewRepo).addRoutes(to: v1)
}
if let perfMetricsRepo = try? factory.makePerfMetricsRepository(authProvider: auth) {
PerfMetricsController(repo: perfMetricsRepo).addRoutes(to: v1)
}
if let diagnosticsRepo = try? factory.makeDiagnosticsRepository(authProvider: auth) {
DiagnosticsController(repo: diagnosticsRepo).addRoutes(to: v1)
}
if let experimentRepo = try? factory.makeExperimentRepository(authProvider: auth) {
ExperimentsController(repo: experimentRepo).addRoutes(to: v1)
}
Expand Down
14 changes: 12 additions & 2 deletions Sources/Domain/Apps/Performance/DiagnosticLogEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,19 @@ public struct DiagnosticLogEntry: Sendable, Equatable, Identifiable, Codable {
}

extension DiagnosticLogEntry: AffordanceProviding {
public var affordances: [String: String] {
public var structuredAffordances: [Affordance] {
[
"listLogs": "asc diagnostic-logs list --signature-id \(signatureId)",
Affordance(key: "listLogs", command: "diagnostic-logs", action: "list", params: ["signature-id": signatureId]),
]
}
}

extension DiagnosticLogEntry: Presentable {
public static var tableHeaders: [String] {
["ID", "Bundle ID", "Version", "OS", "Device", "Event"]
}

public var tableRow: [String] {
[id, bundleId ?? "-", appVersion ?? "-", osVersion ?? "-", deviceType ?? "-", event ?? "-"]
}
}
16 changes: 13 additions & 3 deletions Sources/Domain/Apps/Performance/DiagnosticSignatureInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,20 @@ public struct DiagnosticSignatureInfo: Sendable, Equatable, Identifiable, Codabl
}

extension DiagnosticSignatureInfo: AffordanceProviding {
public var affordances: [String: String] {
public var structuredAffordances: [Affordance] {
[
"listLogs": "asc diagnostic-logs list --signature-id \(id)",
"listSignatures": "asc diagnostics list --build-id \(buildId)",
Affordance(key: "listLogs", command: "diagnostic-logs", action: "list", params: ["signature-id": id]),
Affordance(key: "listSignatures", command: "diagnostics", action: "list", params: ["build-id": buildId]),
]
}
}

extension DiagnosticSignatureInfo: Presentable {
public static var tableHeaders: [String] {
["ID", "Type", "Signature", "Weight", "Trend"]
}

public var tableRow: [String] {
[id, diagnosticType.rawValue, signature, String(weight), insightDirection ?? "-"]
}
}
25 changes: 20 additions & 5 deletions Sources/Domain/Apps/Performance/PerfPowerMetric.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,29 @@ public struct PerformanceMetric: Sendable, Equatable, Identifiable, Codable {
}

extension PerformanceMetric: AffordanceProviding {
public var affordances: [String: String] {
var cmds: [String: String] = [:]
public var structuredAffordances: [Affordance] {
switch parentType {
case .app:
cmds["listAppMetrics"] = "asc perf-metrics list --app-id \(parentId)"
return [Affordance(key: "listAppMetrics", command: "perf-metrics", action: "list", params: ["app-id": parentId])]
case .build:
cmds["listBuildMetrics"] = "asc perf-metrics list --build-id \(parentId)"
return [Affordance(key: "listBuildMetrics", command: "perf-metrics", action: "list", params: ["build-id": parentId])]
}
return cmds
}
}

extension PerformanceMetric: Presentable {
public static var tableHeaders: [String] {
["ID", "Category", "Metric", "Value", "Unit", "Goal"]
}

public var tableRow: [String] {
[
id,
category.rawValue,
metricIdentifier,
latestValue.map { String($0) } ?? "-",
unit ?? "-",
goalValue.map { String($0) } ?? "-",
]
}
}
9 changes: 9 additions & 0 deletions Sources/Domain/Apps/Performance/Performance+RESTRoutes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// REST route registrations for build performance metrics and diagnostics.
/// (`perf-metrics` under an app is registered with the other app children.)
extension RESTPathResolver {
static let _performanceRoutes: Void = {
registerRoute(command: "perf-metrics", parentParam: "build-id", parentSegment: "builds", segment: "perf-metrics")
registerRoute(command: "diagnostics", parentParam: "build-id", parentSegment: "builds", segment: "diagnostics")
registerRoute(command: "diagnostic-logs", parentParam: "signature-id", parentSegment: "diagnostics", segment: "logs")
}()
}
1 change: 1 addition & 0 deletions Sources/Domain/Shared/RESTPathResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ public final class RESTPathResolver: @unchecked Sendable {
_ = _resolutionCenterRoutes
_ = _experimentRoutes
_ = _perfOverviewRoutes
_ = _performanceRoutes
_ = _gameCenterRoutes
_ = _assetLibraryRoutes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public struct SDKLibraryImageRepository: LibraryImageRepository, @unchecked Send
category: AssetCategory?
) async throws -> [LibraryImage] {
let sdk = APIEndpoint.v1.appAssetLibraries.id(libraryId).images.get(parameters: .init(
filterCategory: category.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterState: state.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterCategory: sdkFilter(category),
filterState: sdkFilter(state),
filterID: imageId.map { [$0] },
limit: 200
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,25 +117,25 @@ public struct SDKAssetPlacementRepository: AssetPlacementRepository, @unchecked
switch surface {
case .appStoreVersionLocalization:
let r = APIEndpoint.v1.appStoreVersionLocalizations.id(localizationId).placements.get(parameters: .init(
filterPlacementType: placementType.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterPlacementType: sdkFilter(placementType),
filterPlacementGroup: groups, sort: [.placementGroupPosition], limit: 200, include: [.image, .video]
))
sdk = (r.path, r.query)
case .experimentTreatmentLocalization:
let r = APIEndpoint.v1.appStoreVersionExperimentTreatmentLocalizations.id(localizationId).placements.get(parameters: .init(
filterPlacementType: placementType.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterPlacementType: sdkFilter(placementType),
filterPlacementGroup: groups, sort: [.placementGroupPosition], limit: 200, include: [.image, .video]
))
sdk = (r.path, r.query)
case .customProductPageLocalization:
let r = APIEndpoint.v1.appCustomProductPageLocalizations.id(localizationId).placements.get(parameters: .init(
filterPlacementType: placementType.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterPlacementType: sdkFilter(placementType),
filterPlacementGroup: groups, sort: [.placementGroupPosition], limit: 200, include: [.image, .video]
))
sdk = (r.path, r.query)
case .eventLocalization:
let r = APIEndpoint.v1.appEventLocalizations.id(localizationId).placements.get(parameters: .init(
filterPlacementType: placementType.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterPlacementType: sdkFilter(placementType),
filterPlacementGroup: groups, sort: [.placementGroupPosition], limit: 200, include: [.image, .video]
))
sdk = (r.path, r.query)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public struct SDKLibraryVideoRepository: LibraryVideoRepository, @unchecked Send
category: AssetCategory?
) async throws -> [LibraryVideo] {
let sdk = APIEndpoint.v1.appAssetLibraries.id(libraryId).videos.get(parameters: .init(
filterCategory: category.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterState: state.flatMap { .init(rawValue: $0.rawValue) }.map { [$0] },
filterCategory: sdkFilter(category),
filterState: sdkFilter(state),
filterID: videoId.map { [$0] },
limit: 200
))
Expand Down
10 changes: 10 additions & 0 deletions Sources/Infrastructure/Shared/SDKFilter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// Turns an optional domain enum into the one-element SDK filter array a query parameter takes,
/// or `nil` when there is no value (or the SDK has no matching case).
///
/// The SDK type comes from the parameter being filled. Spelled as a generic function because
/// Swift 6.2 can't infer `.init(rawValue:)` through `value.flatMap { .init(…) }.map { [$0] }`.
func sdkFilter<SDKValue: RawRepresentable, DomainValue: RawRepresentable>(
_ value: DomainValue?
) -> [SDKValue]? where SDKValue.RawValue == String, DomainValue.RawValue == String {
value.flatMap { SDKValue(rawValue: $0.rawValue) }.map { [$0] }
}
Loading
Loading