Send cache targets hits analytics metadata (#4429)

* Add cache hit rate analytics

* Move metadata to command_event

* Add cache metadata to command run

* Show run cache analytics
This commit is contained in:
Marek Fořt 2022-05-11 08:44:31 +02:00 committed by GitHub
parent dd5d42a06d
commit 341047e210
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 424 additions and 36 deletions

View File

@ -10,6 +10,15 @@
"version": "4.6.1"
}
},
{
"package": "AnyCodable",
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
"state": {
"branch": null,
"revision": "f9fda69a7b704d46fb5123005f2f7e43dbb8a0fa",
"version": "0.6.5"
}
},
{
"package": "Checksum",
"repositoryURL": "https://github.com/rnine/Checksum.git",

View File

@ -59,6 +59,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-tools-support-core.git", .upToNextMinor(from: "0.2.5")),
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/fortmarek/ZIPFoundation.git", .revision("9dbe729b90202c19d0fe1010f1430fa75a576cd3")),
.package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.0"),
.package(url: "https://github.com/tuist/GraphViz.git", .branch("tuist")),
.package(url: "https://github.com/SwiftGen/SwiftGen", .exact("6.5.0")),
.package(url: "https://github.com/SwiftGen/StencilSwiftKit.git", .upToNextMajor(from: "2.8.0")),
@ -608,6 +609,7 @@ let package = Package(
.target(
name: "TuistAnalytics",
dependencies: [
.byName(name: "AnyCodable"),
"TuistAsyncQueue",
"TuistCloud",
"TuistCore",

View File

@ -379,6 +379,7 @@ func targets() -> [Target] {
.target(name: "TuistCore"),
.target(name: "TuistGraph"),
.target(name: "TuistLoader"),
.external(name: "AnyCodable"),
],
testDependencies: [
.target(name: "TuistSupportTesting"),

View File

@ -1,3 +1,4 @@
import AnyCodable
import Foundation
import TuistCore
@ -6,7 +7,7 @@ import TuistCore
public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
public let name: String
public let subcommand: String?
public let params: [String: String]
public let params: [String: AnyCodable]
public let commandArguments: [String]
public let durationInMs: Int
public let clientId: String
@ -37,7 +38,7 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
public init(
name: String,
subcommand: String?,
params: [String: String],
params: [String: AnyCodable],
commandArguments: [String],
durationInMs: Int,
clientId: String,

View File

@ -18,6 +18,7 @@ public final class Cache: CacheStoring {
// MARK: - CacheStoring
public func exists(name: String, hash: String) async throws -> Bool {
CacheAnalytics.cacheableTargets.append(name)
for storage in storages {
if try await storage.exists(name: name, hash: hash) {
return true

View File

@ -0,0 +1,11 @@
import Foundation
import TuistSupport
public enum CacheAnalytics {
@Atomic
public static var localCacheTargetsHits: Set<String> = []
@Atomic
public static var remoteCacheTargetsHits: Set<String> = []
@Atomic
public static var cacheableTargets: [String] = []
}

View File

@ -37,9 +37,13 @@ public final class CacheLocalStorage: CacheStoring {
// MARK: - CacheStoring
public func exists(name _: String, hash: String) throws -> Bool {
public func exists(name: String, hash: String) throws -> Bool {
let hashFolder = cacheDirectory.appending(component: hash)
return lookupCompiledArtifact(directory: hashFolder) != nil
let exists = lookupCompiledArtifact(directory: hashFolder) != nil
if exists {
CacheAnalytics.localCacheTargetsHits.insert(name)
}
return exists
}
public func fetch(name _: String, hash: String) throws -> AbsolutePath {

View File

@ -68,7 +68,11 @@ public final class CacheRemoteStorage: CacheStoring {
let successRange = 200 ..< 300
let resource = try cloudCacheResourceFactory.existsResource(name: name, hash: hash)
let (_, response) = try await cloudClient.request(resource)
return successRange.contains(response.statusCode)
let exists = successRange.contains(response.statusCode)
if exists {
CacheAnalytics.remoteCacheTargetsHits.insert(name)
}
return exists
} catch {
if case let HTTPRequestDispatcherError.serverSideError(_, response) = error, response.statusCode == 404 {
return false

View File

@ -1,5 +1,7 @@
import AnyCodable
import ArgumentParser
import Foundation
import TuistCache
struct GenerateCommand: AsyncParsableCommand, HasTrackableParameters {
static var analyticsDelegate: TrackableParametersDelegate?
@ -51,13 +53,6 @@ struct GenerateCommand: AsyncParsableCommand, HasTrackableParameters {
var ignoreCache: Bool = false
func runAsync() async throws {
GenerateCommand.analyticsDelegate?.willRun(withParameters: [
"no_open": String(noOpen),
"xcframeworks": String(xcframeworks),
"no_cache": String(ignoreCache),
"n_targets": String(sources.count),
])
try await GenerateService().run(
path: path,
sources: Set(sources),
@ -66,5 +61,16 @@ struct GenerateCommand: AsyncParsableCommand, HasTrackableParameters {
profile: profile,
ignoreCache: ignoreCache
)
GenerateCommand.analyticsDelegate?.addParameters(
[
"no_open": AnyCodable(noOpen),
"xcframeworks": AnyCodable(xcframeworks),
"no_cache": AnyCodable(ignoreCache),
"n_targets": AnyCodable(sources.count),
"cacheable_targets": AnyCodable(CacheAnalytics.cacheableTargets),
"local_cache_target_hits": AnyCodable(CacheAnalytics.localCacheTargetsHits),
"remote_cache_target_hits": AnyCodable(CacheAnalytics.remoteCacheTargetsHits),
]
)
}
}

View File

@ -1,3 +1,4 @@
import AnyCodable
import ArgumentParser
import Foundation
import GraphViz
@ -58,12 +59,14 @@ struct GraphCommand: AsyncParsableCommand, HasTrackableParameters {
var outputPath: String?
func runAsync() async throws {
GraphCommand.analyticsDelegate?.willRun(withParameters: [
"format": format.rawValue,
"algorithm": layoutAlgorithm.rawValue,
"skip_external_dependencies": String(skipExternalDependencies),
"skip_test_targets": String(skipExternalDependencies),
])
GraphCommand.analyticsDelegate?.addParameters(
[
"format": AnyCodable(format.rawValue),
"algorithm": AnyCodable(layoutAlgorithm.rawValue),
"skip_external_dependencies": AnyCodable(skipExternalDependencies),
"skip_test_targets": AnyCodable(skipExternalDependencies),
]
)
try await GraphService().run(
format: format,
layoutAlgorithm: layoutAlgorithm,

View File

@ -1,3 +1,4 @@
import AnyCodable
import ArgumentParser
import Foundation
import TSCBasic
@ -73,7 +74,11 @@ struct InitCommand: ParsableCommand, HasTrackableParameters {
}
func run() throws {
InitCommand.analyticsDelegate?.willRun(withParameters: ["platform": platform ?? "unknown"])
InitCommand.analyticsDelegate?.addParameters(
[
"platform": AnyCodable(platform ?? "unknown"),
]
)
try InitService().run(
name: name,
platform: platform,

View File

@ -1,3 +1,4 @@
import AnyCodable
import ArgumentParser
import Foundation
import TuistAsyncQueue
@ -7,7 +8,7 @@ import TuistSupport
public struct TrackableCommandInfo {
let name: String
let subcommand: String?
let parameters: [String: String]
let parameters: [String: AnyCodable]
let commandArguments: [String]
let durationInMs: Int
}
@ -16,7 +17,7 @@ public struct TrackableCommandInfo {
public class TrackableCommand: TrackableParametersDelegate {
private var command: ParsableCommand
private let clock: Clock
private var trackedParameters: [String: String] = [:]
private var trackedParameters: [String: AnyCodable] = [:]
private let commandArguments: [String]
private let commandEventFactory: CommandEventFactory
private let asyncQueue: AsyncQueuing
@ -60,8 +61,11 @@ public class TrackableCommand: TrackableParametersDelegate {
try asyncQueue.dispatch(event: commandEvent)
}
func willRun(withParameters parameters: [String: String]) {
trackedParameters = parameters
func addParameters(_ parameters: [String: AnyCodable]) {
trackedParameters.merge(
parameters,
uniquingKeysWith: { _, newKey in newKey }
)
}
private func extractCommandName(from configuration: CommandConfiguration) -> (name: String, subcommand: String?) {

View File

@ -1,3 +1,4 @@
import AnyCodable
import Foundation
/// Commands that conform to `HasTrackableParameters` can report extra parameters that are only known at runtime
@ -8,5 +9,5 @@ protocol HasTrackableParameters {
/// `TrackableParametersDelegate` contains the callback that should be called
/// before running a command, with extra parameters that are only known at runtime
protocol TrackableParametersDelegate: AnyObject {
func willRun(withParameters: [String: String])
func addParameters(_ parameters: [String: AnyCodable])
}

View File

@ -1,3 +1,4 @@
import AnyCodable
import Foundation
@testable import TuistAnalytics
@ -5,7 +6,7 @@ extension CommandEvent {
static func test(
name: String = "generate",
subcommand: String? = nil,
params: [String: String] = [:],
params: [String: AnyCodable] = [:],
commandArguments: [String] = [],
durationInMs: Int = 20,
clientId: String = "123",

View File

@ -1,3 +1,4 @@
import AnyCodable
import ArgumentParser
import Combine
import Foundation
@ -38,7 +39,7 @@ final class TrackableCommandTests: TuistTestCase {
func test_whenParamsHaveFlagTrue_dispatchesEventWithExpectedParameters() async throws {
// Given
makeSubject(flag: true)
let expectedParams = ["flag": "true"]
let expectedParams: [String: AnyCodable] = ["flag": true]
// When
try await subject.run()
@ -53,7 +54,7 @@ final class TrackableCommandTests: TuistTestCase {
func test_whenParamsHaveFlagFalse_dispatchesEventWithExpectedParameters() async throws {
// Given
makeSubject(flag: false)
let expectedParams = ["flag": "false"]
let expectedParams: [String: AnyCodable] = ["flag": false]
// When
try await subject.run()
@ -75,6 +76,6 @@ private struct TestCommand: ParsableCommand, HasTrackableParameters {
static var analyticsDelegate: TrackableParametersDelegate?
func run() throws {
TestCommand.analyticsDelegate?.willRun(withParameters: ["flag": String(flag)])
TestCommand.analyticsDelegate?.addParameters(["flag": AnyCodable(flag)])
}
}

View File

@ -13,6 +13,7 @@ let dependencies = Dependencies(
url: "https://github.com/fortmarek/ZIPFoundation.git",
.revision("9dbe729b90202c19d0fe1010f1430fa75a576cd3")
),
.package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.0"),
.package(url: "https://github.com/rnine/Checksum.git", .upToNextMajor(from: "1.0.2")),
.package(url: "https://github.com/stencilproject/Stencil.git", .upToNextMajor(from: "0.14.1")),
.package(url: "https://github.com/SwiftGen/StencilSwiftKit.git", .upToNextMajor(from: "2.8.0")),

View File

@ -10,6 +10,15 @@
"version": "4.6.1"
}
},
{
"package": "AnyCodable",
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
"state": {
"branch": null,
"revision": "f9fda69a7b704d46fb5123005f2f7e43dbb8a0fa",
"version": "0.6.5"
}
},
{
"package": "Checksum",
"repositoryURL": "https://github.com/rnine/Checksum.git",

View File

@ -12,7 +12,10 @@ class AnalyticsController < APIController
client_id: params[:client_id],
tuist_version: params[:tuist_version],
swift_version: params[:swift_version],
macos_version: params[:macos_version]
macos_version: params[:macos_version],
cacheable_targets: params[:params][:cacheable_targets],
local_cache_target_hits: params[:params][:local_cache_target_hits],
remote_cache_target_hits: params[:params][:remote_cache_target_hits]
)
end
end

View File

@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite';
import React, { useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import CommandEventDetailPageStore from './CommandEventDetailPageStore';
import CacheCardSection from './components/CacheCardSection';
const CommandEventDetailPage = observer(() => {
const client = useApolloClient();
@ -65,6 +66,26 @@ const CommandEventDetailPage = observer(() => {
</Stack>
</Stack>
</Card.Section>
<CacheCardSection
cacheableTargets={
commandEventDetailPageStore.commandEventDetail
.cacheableTargets
}
localCacheTargetHits={
commandEventDetailPageStore.commandEventDetail
.localCacheTargetHits
}
remoteCacheTargetHits={
commandEventDetailPageStore.commandEventDetail
.remoteCacheTargetHits
}
cacheTargetMisses={
commandEventDetailPageStore.cacheTargetMisses
}
cacheTargetHitRate={
commandEventDetailPageStore.cacheTargetHitRate
}
/>
<Card.Section title="Environment">
<Stack vertical>
<Stack>

View File

@ -18,6 +18,51 @@ class CommandEventDetailPageStore {
makeAutoObservable(this);
}
get cacheTargetHitRate(): string {
if (this.commandEventDetail == null) {
return '';
}
const {
cacheableTargets,
localCacheTargetHits,
remoteCacheTargetHits,
} = this.commandEventDetail;
if (
cacheableTargets === null ||
localCacheTargetHits === null ||
remoteCacheTargetHits === null
) {
return '';
}
const cacheTargetHitRate = Math.ceil(
((localCacheTargetHits.length + remoteCacheTargetHits.length) /
cacheableTargets.length) *
100,
);
return `${cacheTargetHitRate} %`;
}
get cacheTargetMisses(): string[] {
if (this.commandEventDetail?.cacheableTargets == null) {
return [];
}
return this.commandEventDetail.cacheableTargets.filter(
(target) => {
return !(
this.commandEventDetail?.localCacheTargetHits?.includes(
target,
) ||
this.commandEventDetail?.remoteCacheTargetHits?.includes(
target,
)
);
},
);
}
async load(commandEventId: string) {
const { data } = await this.client.query<CommandEventQuery>({
query: CommandEventDocument,

View File

@ -32,6 +32,9 @@ describe('CommandEventDetailPageStore', () => {
id: 'command-event-id',
name: 'generate',
subcommand: null,
cacheableTargets: ['Target1', 'Target2', 'Target3', 'Target4'],
localCacheTargetHits: ['Target2', 'Target4'],
remoteCacheTargetHits: ['Target3'],
};
const commandEventDetailFragment = {
clientId: commandEventDetail.clientId,
@ -44,6 +47,9 @@ describe('CommandEventDetailPageStore', () => {
id: commandEventDetail.id,
name: commandEventDetail.name,
subcommand: commandEventDetail.subcommand,
cacheableTargets: commandEventDetail.cacheableTargets,
localCacheTargetHits: commandEventDetail.localCacheTargetHits,
remoteCacheTargetHits: commandEventDetail.remoteCacheTargetHits,
__typename: 'CommandEvent',
} as CommandEventDetailFragment;
client.query.mockResolvedValueOnce({
@ -59,5 +65,11 @@ describe('CommandEventDetailPageStore', () => {
expect(commandEventDetailPageStore.commandEventDetail).toEqual(
commandEventDetail,
);
expect(commandEventDetailPageStore.cacheTargetHitRate).toEqual(
'75 %',
);
expect(commandEventDetailPageStore.cacheTargetMisses).toEqual([
'Target1',
]);
});
});

View File

@ -0,0 +1,80 @@
import React from 'react';
import { Card, TextStyle, Stack } from '@shopify/polaris';
interface CacheCardSectionProps {
cacheableTargets: string[] | null;
localCacheTargetHits: string[] | null;
remoteCacheTargetHits: string[] | null;
cacheTargetMisses: string[];
cacheTargetHitRate: string;
}
const CacheCardSection = ({
cacheableTargets,
localCacheTargetHits,
remoteCacheTargetHits,
cacheTargetMisses,
cacheTargetHitRate,
}: CacheCardSectionProps) => {
if (
cacheableTargets === null ||
localCacheTargetHits === null ||
remoteCacheTargetHits === null
) {
return null;
}
return (
<Card.Section title="Cache">
<Stack vertical>
<Stack>
<TextStyle variation="subdued">Cacheable targets</TextStyle>
<TextStyle>{`Total count: ${cacheableTargets.length}`}</TextStyle>
</Stack>
<TextStyle variation="code">
{cacheableTargets.join(' ')}
</TextStyle>
<Stack>
<TextStyle variation="subdued">
Targets from local cache
</TextStyle>
<TextStyle>{`Total count: ${localCacheTargetHits.length}`}</TextStyle>
{localCacheTargetHits.length > 0 && (
<TextStyle variation="code">
{localCacheTargetHits.join(' ')}
</TextStyle>
)}
</Stack>
<Stack>
<TextStyle variation="subdued">
Targets from remote cache
</TextStyle>
<TextStyle>{`Total count: ${remoteCacheTargetHits.length}`}</TextStyle>
</Stack>
{remoteCacheTargetHits.length > 0 && (
<TextStyle variation="code">
{remoteCacheTargetHits.join(' ')}
</TextStyle>
)}
<Stack>
<TextStyle variation="subdued">
Cache targets missed
</TextStyle>
<TextStyle>{`Total count: ${cacheTargetMisses.length}`}</TextStyle>
</Stack>
{cacheTargetMisses.length > 0 && (
<TextStyle variation="code">
{cacheTargetMisses.join(' ')}
</TextStyle>
)}
<Stack>
<TextStyle variation="subdued">
Total cache hit rate
</TextStyle>
<TextStyle>{cacheTargetHitRate}</TextStyle>
</Stack>
</Stack>
</Card.Section>
);
};
export default CacheCardSection;

View File

@ -9,4 +9,7 @@ fragment CommandEventDetail on CommandEvent {
swiftVersion
macosVersion
createdAt
cacheableTargets
localCacheTargetHits
remoteCacheTargetHits
}

View File

@ -64,13 +64,16 @@ export type CommandAverage = {
export type CommandEvent = {
__typename?: 'CommandEvent';
cacheableTargets?: Maybe<Array<Scalars['String']>>;
clientId: Scalars['String'];
commandArguments: Scalars['String'];
createdAt: Scalars['ISO8601DateTime'];
duration: Scalars['Int'];
id: Scalars['ID'];
localCacheTargetHits?: Maybe<Array<Scalars['String']>>;
macosVersion: Scalars['String'];
name: Scalars['String'];
remoteCacheTargetHits?: Maybe<Array<Scalars['String']>>;
subcommand?: Maybe<Scalars['String']>;
swiftVersion: Scalars['String'];
tuistVersion: Scalars['String'];
@ -431,14 +434,14 @@ export type CommandAveragesQuery = { __typename?: 'Query', commandAverages: Arra
export type CommandEventFragment = { __typename?: 'CommandEvent', id: string, commandArguments: string, duration: number, createdAt: string };
export type CommandEventDetailFragment = { __typename?: 'CommandEvent', id: string, name: string, subcommand?: string | null, commandArguments: string, duration: number, clientId: string, tuistVersion: string, swiftVersion: string, macosVersion: string, createdAt: string };
export type CommandEventDetailFragment = { __typename?: 'CommandEvent', id: string, name: string, subcommand?: string | null, commandArguments: string, duration: number, clientId: string, tuistVersion: string, swiftVersion: string, macosVersion: string, createdAt: string, cacheableTargets?: Array<string> | null, localCacheTargetHits?: Array<string> | null, remoteCacheTargetHits?: Array<string> | null };
export type CommandEventQueryVariables = Exact<{
commandEventId: Scalars['ID'];
}>;
export type CommandEventQuery = { __typename?: 'Query', commandEvent: { __typename?: 'CommandEvent', id: string, name: string, subcommand?: string | null, commandArguments: string, duration: number, clientId: string, tuistVersion: string, swiftVersion: string, macosVersion: string, createdAt: string } };
export type CommandEventQuery = { __typename?: 'Query', commandEvent: { __typename?: 'CommandEvent', id: string, name: string, subcommand?: string | null, commandArguments: string, duration: number, clientId: string, tuistVersion: string, swiftVersion: string, macosVersion: string, createdAt: string, cacheableTargets?: Array<string> | null, localCacheTargetHits?: Array<string> | null, remoteCacheTargetHits?: Array<string> | null } };
export type CommandEventsQueryVariables = Exact<{
projectId: Scalars['ID'];
@ -585,6 +588,9 @@ export const CommandEventDetailFragmentDoc = gql`
swiftVersion
macosVersion
createdAt
cacheableTargets
localCacheTargetHits
remoteCacheTargetHits
}
`;
export const PendingInvitationFragmentDoc = gql`

View File

@ -11,6 +11,9 @@ export interface CommandEventDetail {
swiftVersion: string;
macosVersion: string;
createdAt: Date;
cacheableTargets: string[] | null;
localCacheTargetHits: string[] | null;
remoteCacheTargetHits: string[] | null;
}
export const mapCommandEventDetail = ({
@ -24,6 +27,9 @@ export const mapCommandEventDetail = ({
swiftVersion,
macosVersion,
createdAt,
cacheableTargets,
localCacheTargetHits,
remoteCacheTargetHits,
}: CommandEventDetailFragment) => {
return {
id,
@ -36,5 +42,8 @@ export const mapCommandEventDetail = ({
swiftVersion,
macosVersion,
createdAt: new Date(createdAt),
cacheableTargets,
localCacheTargetHits,
remoteCacheTargetHits,
} as CommandEventDetail;
};

View File

@ -11,6 +11,21 @@ module Types
field :tuist_version, String, null: false
field :swift_version, String, null: false
field :macos_version, String, null: false
field :cacheable_targets, [String], null: true
field :local_cache_target_hits, [String], null: true
field :remote_cache_target_hits, [String], null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
def cacheable_targets
object.cacheable_targets.nil? ? nil : object.cacheable_targets.split(";")
end
def local_cache_target_hits
object.local_cache_target_hits.nil? ? nil : object.local_cache_target_hits.split(";")
end
def remote_cache_target_hits
object.remote_cache_target_hits.nil? ? nil : object.remote_cache_target_hits.split(";")
end
end
end

View File

@ -2,7 +2,8 @@
class CommandEventCreateService < ApplicationService
attr_reader :account_name, :project_name, :user, :name, :subcommand, :command_arguments, :duration, :client_id,
:tuist_version, :swift_version, :macos_version
:tuist_version, :swift_version, :macos_version, :cacheable_targets, :local_cache_target_hits,
:remote_cache_target_hits
def initialize(
project_slug:,
@ -14,7 +15,10 @@ class CommandEventCreateService < ApplicationService
client_id:,
tuist_version:,
swift_version:,
macos_version:
macos_version:,
cacheable_targets:,
local_cache_target_hits:,
remote_cache_target_hits:
)
super()
split_project_slug = project_slug.split("/")
@ -29,6 +33,9 @@ class CommandEventCreateService < ApplicationService
@tuist_version = tuist_version
@swift_version = swift_version
@macos_version = macos_version
@cacheable_targets = cacheable_targets
@local_cache_target_hits = local_cache_target_hits
@remote_cache_target_hits = remote_cache_target_hits
end
def call
@ -43,7 +50,10 @@ class CommandEventCreateService < ApplicationService
tuist_version: tuist_version,
swift_version: swift_version,
macos_version: macos_version,
project: project
project: project,
cacheable_targets: cacheable_targets.nil? ? nil : cacheable_targets.join(";"),
local_cache_target_hits: local_cache_target_hits.nil? ? nil : local_cache_target_hits.join(";"),
remote_cache_target_hits: remote_cache_target_hits.nil? ? nil : remote_cache_target_hits.join(";")
)
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddCacheMetadataToCommandEvent < ActiveRecord::Migration[7.0]
def change
change_table(:command_events) do |t|
t.string(:cacheable_targets)
t.string(:local_cache_target_hits)
t.string(:remote_cache_target_hits)
end
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_04_10_132610) do
ActiveRecord::Schema[7.0].define(version: 2022_05_05_193115) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -37,6 +37,9 @@ ActiveRecord::Schema[7.0].define(version: 2022_04_10_132610) do
t.bigint "project_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "cacheable_targets"
t.string "local_cache_target_hits"
t.string "remote_cache_target_hits"
t.index ["project_id"], name: "index_command_events_on_project_id"
end

View File

@ -58,13 +58,16 @@ type CommandAverage {
}
type CommandEvent {
cacheableTargets: [String!]
clientId: String!
commandArguments: String!
createdAt: ISO8601DateTime!
duration: Int!
id: ID!
localCacheTargetHits: [String!]
macosVersion: String!
name: String!
remoteCacheTargetHits: [String!]
subcommand: String
swiftVersion: String!
tuistVersion: String!

View File

@ -370,6 +370,28 @@
"name": "CommandEvent",
"description": null,
"fields": [
{
"name": "cacheableTargets",
"description": null,
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clientId",
"description": null,
@ -460,6 +482,28 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "localCacheTargetHits",
"description": null,
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "macosVersion",
"description": null,
@ -496,6 +540,28 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "remoteCacheTargetHits",
"description": null,
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "subcommand",
"description": null,

View File

@ -20,11 +20,48 @@ class CommandEventCreateServiceTest < ActiveSupport::TestCase
client_id: "client id",
tuist_version: "3.1.0",
swift_version: "5.5.0",
macos_version: "12.1.0"
macos_version: "12.1.0",
cacheable_targets: nil,
local_cache_target_hits: nil,
remote_cache_target_hits: nil
)
# Then
assert_equal project.command_events, [got]
assert_nil got.cacheable_targets
assert_nil got.local_cache_target_hits
assert_nil got.remote_cache_target_hits
assert_equal "fetch", got.name
end
test "creates a cache warm command event for a given project" do
# Given
user = User.create!(email: "test@cloud.tuist.io", password: Devise.friendly_token.first(16))
account = user.account
project = Project.create!(name: "tuist-project", account_id: account.id, token: Devise.friendly_token.first(16))
# When
got = CommandEventCreateService.call(
project_slug: "#{account.name}/#{project.name}",
user: user,
name: "cache",
subcommand: "warm",
command_arguments: ["cache", "warm"],
duration: 120,
client_id: "client id",
tuist_version: "3.1.0",
swift_version: "5.5.0",
macos_version: "12.1.0",
cacheable_targets: ["Target1", "Target2", "Target3", "Target4"],
local_cache_target_hits: ["Target1"],
remote_cache_target_hits: ["Target2", "Target4"]
)
# Then
assert_equal project.command_events, [got]
assert_equal "cache", got.name
assert_equal "Target1;Target2;Target3;Target4", got.cacheable_targets
assert_equal "Target1", got.local_cache_target_hits
assert_equal "Target2;Target4", got.remote_cache_target_hits
end
end