diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/Wizard.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/Wizard.cs index 05ccac347..229f8c306 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/Wizard.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/Wizard.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Sentry.Extensibility; +using Sentry.Unity.Editor.WizardApi; using UnityEditor; using UnityEngine; @@ -40,7 +41,7 @@ public static void Start(IDiagnosticLogger logger) private void StartLoder() { - _task = new WizardLoader(this, _logger); + _task = new WizardLoader(_logger); Task.Run(async () => Response = await _task.Load()).ContinueWith(t => { if (t.Exception is not null) @@ -63,11 +64,11 @@ private void OnGUI() EditorGUILayout.Space(); - if (Response.Projects.Count == 0) + if (Response.projects.Count == 0) { wizardConfiguration = new WizardConfiguration { - Token = Response.ApiKeys!.Token + Token = Response.apiKeys!.token }; EditorGUILayout.LabelField("There don't seem to be any projects in your sentry.io account."); @@ -80,11 +81,11 @@ private void OnGUI() var firstEntry = new string(' ', 60); // sort "unity" projects first - Response.Projects.Sort((a, b) => + Response.projects.Sort((a, b) => { if (a.IsUnity == b.IsUnity) { - return (a.Name ?? "").CompareTo(b.Name ?? ""); + return (a.name ?? "").CompareTo(b.name ?? ""); } else if (a.IsUnity) { @@ -96,7 +97,7 @@ private void OnGUI() } }); - var orgsAndProjects = Response.Projects.GroupBy(k => k.Organization!.Name, v => v).ToArray(); + var orgsAndProjects = Response.projects.GroupBy(k => k.organization!.name, v => v).ToArray(); // if only one org if (orgsAndProjects.Length == 1) @@ -111,7 +112,7 @@ private void OnGUI() if (_orgSelected > 0) { - _projectSelected = EditorGUILayout.Popup("Project", _projectSelected, orgsAndProjects[_orgSelected - 1].Select(v => $"{v.Name} - ({v.Slug})").ToArray() + _projectSelected = EditorGUILayout.Popup("Project", _projectSelected, orgsAndProjects[_orgSelected - 1].Select(v => $"{v.name} - ({v.slug})").ToArray() .Prepend(firstEntry).ToArray()); } @@ -120,10 +121,10 @@ private void OnGUI() var proj = orgsAndProjects[_orgSelected - 1].ToArray()[_projectSelected - 1]; wizardConfiguration = new WizardConfiguration { - Token = Response.ApiKeys!.Token, - Dsn = proj.Keys.First().Dsn!.Public, - OrgSlug = proj.Organization!.Slug, - ProjectSlug = proj.Slug, + Token = Response.apiKeys!.token, + Dsn = proj.keys.First().dsn!.@public, + OrgSlug = proj.organization!.slug, + ProjectSlug = proj.slug, }; } } @@ -218,48 +219,6 @@ internal static void OpenUrl(string url) Application.OpenURL(parsedUri.ToString()); } - - internal class WizardStep1Response - { - public string? Hash { get; set; } - } - internal class WizardStep2Response - { - public ApiKeys? ApiKeys { get; set; } - public List Projects { get; set; } = new List(0); - } - - internal class ApiKeys - { - public string? Token { get; set; } - } - - internal class Project - { - public Organization? Organization { get; set; } - public string? Slug { get; set; } - public string? Name { get; set; } - public string? Platform { get; set; } - public IEnumerable Keys { get; set; } = Enumerable.Empty(); - - public bool IsUnity => string.Equals(Platform, "unity", StringComparison.InvariantCultureIgnoreCase); - } - - internal class Key - { - public Dsn? Dsn { get; set; } - } - - internal class Dsn - { - public string? Public { get; set; } - } - - internal class Organization - { - public string? Name { get; set; } - public string? Slug { get; set; } - } } internal class WizardConfiguration @@ -279,7 +238,6 @@ internal WizardCancelled(string message, Exception innerException) : base(messag internal class WizardLoader { - private Wizard _wizard; private IDiagnosticLogger _logger; internal float _progress = 0.0f; internal string _progressText = ""; @@ -289,15 +247,8 @@ internal class WizardLoader internal Action? _uiAction = null; private const int StepCount = 5; - private readonly JsonSerializerOptions _serializeOptions = new() + public WizardLoader(IDiagnosticLogger logger) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - - public WizardLoader(Wizard wizard, IDiagnosticLogger logger) - { - _wizard = wizard; _logger = logger; } @@ -316,9 +267,9 @@ private void Progress(string status, float current) _progress = current; } - internal async Task Load() + internal async Task Load() { - Wizard.WizardStep2Response? response = null; + WizardStep2Response? response = null; try { Progress("Started", 1); @@ -326,13 +277,13 @@ private void Progress(string status, float current) Progress("Connecting to sentry.io settings wizard...", 2); var http = new HttpClient(); var resp = await http.GetAsync("https://sentry.io/api/0/wizard/").ConfigureAwait(false); - var wizardHashResponse = await DeserializeJson(resp); + var wizardHashResponse = await DeserializeJson(resp); Progress("Opening sentry.io in the default browser...", 3); - await RunOnUiThread(() => Wizard.OpenUrl($"https://sentry.io/account/settings/wizard/{wizardHashResponse.Hash}/")); + await RunOnUiThread(() => Wizard.OpenUrl($"https://sentry.io/account/settings/wizard/{wizardHashResponse.hash}/")); // Poll https://sentry.io/api/0/wizard/hash/ - var pollingUrl = $"https://sentry.io/api/0/wizard/{wizardHashResponse.Hash}/"; + var pollingUrl = $"https://sentry.io/api/0/wizard/{wizardHashResponse.hash}/"; Progress("Waiting for the the response from the browser session...", 4); @@ -343,7 +294,7 @@ private void Progress(string status, float current) resp = await http.GetAsync(pollingUrl).ConfigureAwait(false); if (resp.StatusCode != HttpStatusCode.BadRequest) // not ready yet { - response = await DeserializeJson(resp).ConfigureAwait(false); + response = await DeserializeJson(resp).ConfigureAwait(false); break; } } @@ -378,9 +329,11 @@ private void Progress(string status, float current) private async Task DeserializeJson(HttpResponseMessage response) { var content = await response.EnsureSuccessStatusCode().Content.ReadAsByteArrayAsync().ConfigureAwait(false); - return JsonSerializer.Deserialize(content, _serializeOptions)!; + return DeserializeJson(System.Text.Encoding.UTF8.GetString(content)); } + internal T DeserializeJson(string json) => JsonUtility.FromJson(json); + private Task RunOnUiThread(Action callback) { var tcs = new TaskCompletionSource(); diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/WizardApi.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/WizardApi.cs new file mode 100644 index 000000000..67e6c9387 --- /dev/null +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/WizardApi.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Sentry.Extensibility; +using UnityEditor; +using UnityEngine; + +namespace Sentry.Unity.Editor.WizardApi +{ + [Serializable] + internal class WizardStep1Response + { + public string? hash; + } + + [Serializable] + internal class WizardStep2Response + { + public ApiKeys? apiKeys; + public List projects = new List(0); + } + + [Serializable] + internal class ApiKeys + { + public string? token; + } + + [Serializable] + internal class Project + { + public Organization? organization; + public string? slug; + public string? name; + public string? platform; + public List? keys; + + public bool IsUnity => string.Equals(platform, "unity", StringComparison.InvariantCultureIgnoreCase); + } + + [Serializable] + internal class Key + { + public Dsn? dsn; + } + + [Serializable] + internal class Dsn + { + public string? @public; + } + + [Serializable] + internal class Organization + { + public string? name; + public string? slug; + } + + interface WizardApi { + + } +} diff --git a/test/Sentry.Unity.Editor.Tests/WizardTests.cs b/test/Sentry.Unity.Editor.Tests/WizardTests.cs new file mode 100644 index 000000000..e4212782d --- /dev/null +++ b/test/Sentry.Unity.Editor.Tests/WizardTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Sentry.Unity.Editor.ConfigurationWindow; +using Sentry.Unity.Editor.WizardApi; +using UnityEngine.TestTools; +using Sentry.Unity.Tests.SharedClasses; + +namespace Sentry.Unity.Editor.Tests +{ + public sealed class WizardJson + { + [Test] + public void Step1Response() + { + var sut = new WizardLoader(new TestLogger()); + + var parsed = sut.DeserializeJson("{\"hash\":\"foo\"}"); + + Assert.AreEqual("foo", parsed.hash); + } + + [Test] + public void Step2Response() + { + var sut = new WizardLoader(new TestLogger()); + + var json = "{\"apiKeys\":{\"id\":\"key-1\",\"scopes\":[\"org:read\",\"project:read\",\"project:releases\",\"project:write\"],\"application\":null,\"expiresAt\":null,\"dateCreated\":\"2022-03-02T10:37:56.385524Z\",\"state\":null,\"token\":\"api-key-token\",\"refreshToken\":null},\"projects\":[{\"id\":\"project-1\",\"slug\":\"project-slug\",\"name\":\"personal\",\"isPublic\":false,\"isBookmarked\":false,\"color\":\"#3fb7bf\",\"dateCreated\":\"2022-01-15T20:05:53.883628Z\",\"firstEvent\":\"2022-01-15T20:15:10.171648Z\",\"firstTransactionEvent\":false,\"hasSessions\":true,\"features\":[\"alert-filters\",\"issue-alerts-targeting\",\"minidump\",\"performance-suspect-spans-ingestion\",\"race-free-group-creation\",\"similarity-indexing\",\"similarity-view\",\"releases\"],\"status\":\"active\",\"platform\":\"flutter\",\"isInternal\":false,\"isMember\":false,\"hasAccess\":true,\"avatar\":{\"avatarType\":\"letter_avatar\",\"avatarUuid\":null},\"organization\":{\"id\":\"org-1\",\"slug\":\"org-slug\",\"status\":{\"id\":\"active\",\"name\":\"active\"},\"name\":\"organization-1\",\"dateCreated\":\"2022-01-15T20:03:49.620687Z\",\"isEarlyAdopter\":false,\"require2FA\":false,\"requireEmailVerification\":false,\"avatar\":{\"avatarType\":\"letter_avatar\",\"avatarUuid\":null},\"features\":[\"onboarding\",\"ondemand-budgets\",\"slack-overage-notifications\",\"dashboards-template\",\"discover-frontend-use-events-endpoint\",\"integrations-stacktrace-link\",\"crash-rate-alerts\",\"org-subdomains\",\"performance-dry-run-mep\",\"mobile-app\",\"custom-event-title\",\"advanced-search\",\"widget-library\",\"auto-start-free-trial\",\"release-health-return-metrics\",\"minute-resolution-sessions\",\"capture-lead\",\"invite-members-rate-limits\",\"alert-crash-free-metrics\",\"alert-wizard-v3\",\"images-loaded-v2\",\"duplicate-alert-rule\",\"performance-autogroup-sibling-spans\",\"performance-ops-breakdown\",\"new-widget-builder-experience-design\",\"performance-suspect-spans-view\",\"unified-span-view\",\"performance-frontend-use-events-endpoint\",\"widget-viewer-modal\",\"event-attachments\",\"symbol-sources\",\"performance-span-histogram-view\",\"intl-sales-tax\",\"metrics-extraction\",\"performance-view\",\"new-weekly-report\",\"performance-span-tree-autoscroll\",\"metric-alert-snql\",\"shared-issues\",\"dashboard-grid-layout\",\"open-membership\"]},\"keys\":[{\"id\":\"key-1\",\"name\":\"Default\",\"label\":\"Default\",\"public\":\"public-key\",\"secret\":\"secret-key\",\"projectId\":12345,\"isActive\":true,\"rateLimit\":null,\"dsn\":{\"secret\":\"dsn-secret\",\"public\":\"dsn-public\",\"csp\":\"\",\"security\":\"\",\"minidump\":\"\",\"unreal\":\"\",\"cdn\":\"\"},\"browserSdkVersion\":\"6.x\",\"browserSdk\":{\"choices\":[[\"latest\",\"latest\"],[\"7.x\",\"7.x\"],[\"6.x\",\"6.x\"],[\"5.x\",\"5.x\"],[\"4.x\",\"4.x\"]]},\"dateCreated\":\"2022-01-15T20:05:53.895882Z\"}]},{\"id\":\"project-2\",\"slug\":\"trending-movies\",\"name\":\"trending-movies\",\"isPublic\":false,\"isBookmarked\":false,\"color\":\"#bfb93f\",\"dateCreated\":\"2022-06-16T16:34:36.833418Z\",\"firstEvent\":null,\"firstTransactionEvent\":false,\"hasSessions\":false,\"features\":[\"alert-filters\",\"custom-inbound-filters\",\"data-forwarding\",\"discard-groups\",\"issue-alerts-targeting\",\"minidump\",\"performance-suspect-spans-ingestion\",\"race-free-group-creation\",\"rate-limits\",\"servicehooks\",\"similarity-indexing\",\"similarity-indexing-v2\",\"similarity-view\",\"similarity-view-v2\"],\"status\":\"active\",\"platform\":\"apple-ios\",\"isInternal\":false,\"isMember\":false,\"hasAccess\":true,\"avatar\":{\"avatarType\":\"letter_avatar\",\"avatarUuid\":null},\"organization\":{\"id\":\"organization-2\",\"slug\":\"sentry-sdks\",\"status\":{\"id\":\"active\",\"name\":\"active\"},\"name\":\"Sentry SDKs\",\"dateCreated\":\"2020-09-14T17:28:14.933511Z\",\"isEarlyAdopter\":true,\"require2FA\":false,\"requireEmailVerification\":false,\"avatar\":{\"avatarType\":\"upload\",\"avatarUuid\":\"\"},\"features\":[\"mobile-screenshots\",\"integrations-ticket-rules\",\"ondemand-budgets\",\"dashboards-template\",\"metric-alert-chartcuterie\",\"reprocessing-v2\",\"filters-and-sampling\",\"grouping-stacktrace-ui\",\"discover-frontend-use-events-endpoint\",\"grouping-title-ui\",\"app-store-connect-multiple\",\"crash-rate-alerts\",\"org-subdomains\",\"performance-dry-run-mep\",\"mobile-app\",\"grouping-tree-ui\",\"custom-event-title\",\"widget-library\",\"advanced-search\",\"auto-start-free-trial\",\"global-views\",\"integrations-chat-unfurl\",\"release-health-return-metrics\",\"integrations-stacktrace-link\",\"dashboards-basic\",\"minute-resolution-sessions\",\"discover-basic\",\"capture-lead\",\"alert-crash-free-metrics\",\"data-forwarding\",\"custom-symbol-sources\",\"sso-saml2\",\"integrations-alert-rule\",\"alert-wizard-v3\",\"invite-members\",\"team-insights\",\"integrations-issue-sync\",\"images-loaded-v2\",\"duplicate-alert-rule\",\"performance-autogroup-sibling-spans\",\"monitors\",\"new-widget-builder-experience-design\",\"dashboards-edit\",\"integrations-issue-basic\",\"performance-ops-breakdown\",\"performance-suspect-spans-view\",\"unified-span-view\",\"related-events\",\"integrations-event-hooks\",\"performance-frontend-use-events-endpoint\",\"sso-basic\",\"widget-viewer-modal\",\"event-attachments\",\"symbol-sources\",\"performance-span-histogram-view\",\"new-widget-builder-experience\",\"profiling\",\"dashboards-releases\",\"metrics-extraction\",\"integrations-incident-management\",\"new-weekly-report\",\"performance-view\",\"performance-span-tree-autoscroll\",\"open-membership\",\"metric-alert-snql\",\"shared-issues\",\"integrations-codeowners\",\"change-alerts\",\"dashboard-grid-layout\",\"baa\",\"discover-query\",\"alert-filters\",\"incidents\",\"relay\"]},\"keys\":[{\"id\":\"key-2\",\"name\":\"Default\",\"label\":\"Default\",\"public\":\"public\",\"secret\":\"secret\",\"projectId\":6789,\"isActive\":true,\"rateLimit\":null,\"dsn\":{\"secret\":\"dsn-secret\",\"public\":\"dsn-public\",\"csp\":\"\",\"security\":\"\",\"minidump\":\"\",\"unreal\":\"\",\"cdn\":\"\"},\"browserSdkVersion\":\"7.x\",\"browserSdk\":{\"choices\":[[\"latest\",\"latest\"],[\"7.x\",\"7.x\"],[\"6.x\",\"6.x\"],[\"5.x\",\"5.x\"],[\"4.x\",\"4.x\"]]},\"dateCreated\":\"2022-06-16T16:34:36.845197Z\"}]}]}"; + var parsed = sut.DeserializeJson(json); + + Assert.NotNull(parsed.apiKeys); + Assert.AreEqual("api-key-token", parsed.apiKeys!.token); + + Assert.NotNull(parsed.projects); + Assert.AreEqual(2, parsed.projects.Count); + var project = parsed.projects[0]; + Assert.AreEqual("project-slug", project.slug); + Assert.AreEqual("personal", project.name); + Assert.AreEqual("flutter", project.platform); + + Assert.IsFalse(project.IsUnity); + project.platform = "unity"; + Assert.IsTrue(project.IsUnity); + + Assert.NotNull(project.organization); + var org = project.organization!; + Assert.AreEqual("organization-1", org.name); + Assert.AreEqual("org-slug", org.slug); + + Assert.NotNull(project.keys); + Assert.AreEqual(1, project.keys!.Count); + var key = project.keys[0]; + Assert.NotNull(key.dsn); + Assert.AreEqual("dsn-public", key.dsn!.@public); + } + } +}