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

☁️ Add analytics option to Config.Cloud to enable sending analytics event to cloud backend #3547

Merged
merged 17 commits into from
Oct 19, 2021

Conversation

danieleformichelli
Copy link
Collaborator

Resolves #3527

Short description 📝

Describe here the purpose of your PR.

Checklist ✅

  • The code architecture and patterns are consistent with the rest of the codebase.
  • The changes have been tested following the documented guidelines.
  • The CHANGELOG.md has been updated to reflect the changes. In case of a breaking change, it's been flagged as such.
  • In case the PR introduces changes that affect users, the documentation has been updated.

Copy link
Member

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nits but otherwise looks good to me. However, I am not extremely familiar with this area, so I'd definitely wait for someone else also having a look!

Sources/TuistAnalytics/TuistAnalytics.swift Outdated Show resolved Hide resolved
Copy link
Contributor

@pepicrft pepicrft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome @danyf90. Thanks for making it happen. As part of Tuist Cloud we are planning to have a home dashboard where I think we can present the data sent by this logic.

Package.resolved Outdated Show resolved Hide resolved
self.cloudConfig = cloudConfig
}

func storeResource(encodedCommandEvent: Data) -> CloudStoreResource {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick:

  • I'd rename the method to create. The resource is already in the name of the factory and create aligns more with API conventions.
  • I'd turn Data into a serializable object that matches the schema expected don't the API side. If the strong contract between the client and the server becomes too limiting, we can figure out ways to add more flexibility.
  • I'd rename CloudStoreResource to CloudAnalyticsCreateResource.

Moreover, I'd add a unit test to ensure the resource that we get from the factory is the expected one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the naming, we are not really creating anything here 🤔
I have used store for consistency with the cache API, but maybe in this case send is more appropriate?


public final class TuistAnalytics {
public static func bootstrap() {
AsyncQueue.sharedInstance.register(dispatcher: TuistAnalyticsDispatcher())
public static func bootstrap(configLoader: ConfigLoader = ConfigLoader(manifestLoader: ManifestLoader())) throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent with the rest of the codebase, I'd avoid loading the configuration as part of bootstrapping analytics. If the bootstrapping of analytics requires cloudReporting: Bool, let's make that partof the method's signature and pass it from outside:

public static bootstrap(cloudReporting: Bool)

The command's service is the one loading the config and passing it down to the components that need it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, but this part is common across all commands, and hence bootstrapped directly in the main.swift. Would you put this logic there?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd create another static method on this component such as loadCloudConfig that would handle that logic. It's not ideal but I agree with the expectation of boostrap() not containing config loading. I'd then call this method explicitly in main.swift

Sources/TuistGraph/Models/Cloud.swift Outdated Show resolved Hide resolved
@@ -6,7 +6,7 @@ import TuistSupport
if CommandLine.arguments.contains("--verbose") { try? ProcessEnv.setVar(Constants.EnvironmentVariables.verbose, value: "true") }

TuistSupport.LogOutput.bootstrap()
TuistAnalytics.bootstrap()
try TuistAnalytics.bootstrap()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why the bootstrap business logic was modified to read the config. What about moving this to the commands?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean adding TuistAnalytics.bootstrap() to each command?
Given we know it is for each command it would be better to handle it upfront, avoiding duplications and forgetting to add it in some commands

Sources/TuistAnalytics/TuistAnalyticsDispatcher.swift Outdated Show resolved Hide resolved
@pepicrft
Copy link
Contributor

👋🏼 I pushed some tests. I slept over the idea of loading the config as part of initializing analytics, and I think it's a bad idea for user experience because loading the config invokes the compiler, and that might take some time if the cache is not warmed. I'm pondering the idea of using environment variables instead for passing the cloud configuration:

TUIST_CONFIG_CLOUD_URL = ...
TUIST_CONFIG_CLOUD_PROJECT_ID = ...

Because we'd like developers to have those as part of the project, I'm thinking about having a .env file under Tuist/. It's a widespread convention in other ecosystems. Because we don't need to invoke the compiler to read that file, we can keep the loading logic as it is, but without having to read the config file, only reading the .env file.

Thoughts @danyf90 / @fortmarek ?

@pepicrft
Copy link
Contributor

I gave another thought, and I think we can keep things as they are, but turn the Config.swift manifest into another format that can be loaded and parsed fast. For example, .yaml or .toml. The only benefit we'd lose is the experience of writing them using Xcode. I think it's a good long-term trade-off that will slightly improve the execution time of commands.

@danieleformichelli
Copy link
Collaborator Author

I gave another thought, and I think we can keep things as they are, but turn the Config.swift manifest into another format that can be loaded and parsed fast. For example, .yaml or .toml. The only benefit we'd lose is the experience of writing them using Xcode. I think it's a good long-term trade-off that will slightly improve the execution time of commands.

@pepibumur Isn't JSON enough for our use case, and easier to parse from Swift?

We might even go hybrid and have the logic that:

  • when reading commands, it reads from Config.json
  • when tuist editing, deserializes the json to a swift to easily edit in Xcode, and on save it serializes back to JSON

Apart from the above, I would expect the parsing of Config.swift should be quick anyway, do we have any benchmark on it? Also, it will be parsed anyway at some point of the command logic, maybe we can reuse the one parsed for analytics, and "gain" back the time we have spent earlier?

@fortmarek
Copy link
Member

when tuist editing, deserializes the json to a swift to easily edit in Xcode, and on save it serializes back to JSON

The issue with this is you can edit the file without using tuist edit. But I'd also look into how much time we will save here by not compiling Config.swift to make sure the trade-off is worth it.

@danieleformichelli
Copy link
Collaborator Author

when tuist editing, deserializes the json to a swift to easily edit in Xcode, and on save it serializes back to JSON

Another issue is that you would lose any "executable code" you have in the swift file and the next tuist edit would start with the plain file, which is probably not acceptable 🤔

If the performance problem is not too big I would probably address it separately from this PR, what do you think @fortmarek and @pepibumur ?

@pepicrft
Copy link
Contributor

@pepibumur Isn't JSON enough for our use case, and easier to parse from Swift?

JSON and YAML can get very messy and therefore I propose using .toml instead because it's more human-readable. We load the content in memory and parse it

The issue with this is you can edit the file without using tuist edit. But I'd also look into how much time we will save here by not compiling Config.swift to make sure the trade-off is worth it.

If you have numbers that'd be great but considering it's invoking the compiler and the dynamic linker, I doubt it's under 300 ms.

Another issue is that you would lose any "executable code" you have in the swift file and the next tuist edit would start with the plain file, which is probably not acceptable 🤔

You are right. Choosing .swift for configuring Tuist, was a natural step after using Swift for the other files. However, it was never intended to be used like other manifests (i.e. writing executable code or sharing content through helpers). I'll open a Pr documenting that we discourage that in the Config.swift file.

If the performance problem is not too big I would probably address it separately from this PR, what do you think @fortmarek and @pepibumur ?

Let's refrain from merging it until we address the other concern. If we merge and introduce a performance regression we might be worsening the DX using the tool, and the user experience should always be first when prioritizing.

@danieleformichelli
Copy link
Collaborator Author

@fortmarek / @pepibumur I have pushed some changes to share the ConfigLoader between the analytics bootstrap and the command execution, which should mitigate the problem of having to parse the config there.

Let me know what you think about it 🙏

@pepicrft
Copy link
Contributor

@fortmarek / @pepibumur I have pushed some changes to share the ConfigLoader between the analytics bootstrap and the command execution, which should mitigate the problem of having to parse the config there.

Turning the config loader into a singleton is not necessary because the manifest loader is already caching the JSON-representation of the manifests (plus it makes the instance a place for storing state that might make some code flows non-deterministic).
The numbers I get for cold manifest loading are around 0.22 seconds, which is reasonable. Thefere, let's revert the ConfigLoader.shared and stick to the ConfigLoader() constructor. If CI is green after that we can merge the PR.

@danieleformichelli
Copy link
Collaborator Author

Done ✅ 🤞

@pepicrft pepicrft enabled auto-merge (squash) October 18, 2021 15:40
@pepicrft pepicrft merged commit b6087af into main Oct 19, 2021
@pepicrft pepicrft deleted the feature/cloud/analytics branch October 19, 2021 15:55
kwridan added a commit that referenced this pull request Oct 20, 2021
- A race between 2 PRs caused build failures on `main`
  - #3547
  - #3568
- Adding missing properties to fix compilation errors

Test Plan:

- Verify the project compiles (when building all targets)
@kwridan kwridan mentioned this pull request Oct 20, 2021
2 tasks
fortmarek pushed a commit that referenced this pull request Oct 20, 2021
- A race between 2 PRs caused build failures on `main`
  - #3547
  - #3568
- Adding missing properties to fix compilation errors

Test Plan:

- Verify the project compiles (when building all targets)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants