diff --git a/cmd/symbols/upload.go b/cmd/symbols/upload.go index 980b95e3..f5e8fd24 100644 --- a/cmd/symbols/upload.go +++ b/cmd/symbols/upload.go @@ -52,9 +52,19 @@ const ( // no sources. sourcePathFlag = "source-path" - defaultPath = "." + defaultPath = "." + + // defaultBackendUrl is the observability API for LaunchDarkly production, which + // is what defaultBackendURLFor derives for the default base URI. defaultBackendUrl = "https://pri.observability.app.launchdarkly.com" + // Every LaunchDarkly instance publishes the observability API under its own + // host, named for the instance the app is served from: staging's app at + // ld-stg.launchdarkly.com has its API at pri.observability.ld-stg.launchdarkly.com. + // "pri" is the authenticated graph, which is the one that hands out upload URLs. + launchDarklyDomain = "launchdarkly.com" + observabilityAPIPrefix = "pri.observability." + // reactNativeSymbolsIDPrefix is the storage "version" segment for symbols-id // addressed JS maps (Symbols Id Lane). Keys become _sym/js/id//, // matching what the symbolication backend derives from the reported symbols id. @@ -218,7 +228,7 @@ func runE(client resources.Client) func(cmd *cobra.Command, args []string) error skipExisting := !viper.GetBool(noSkipExistingFlag) if backendUrl == "" { - backendUrl = defaultBackendUrl + backendUrl = defaultBackendURLFor(viper.GetString(cliflags.BaseURIFlag)) } // Apple dSYMs take a dedicated path: they are compiled to per-arch .dsymmap @@ -518,6 +528,26 @@ func readSymbolsIDFile(filePath string) string { return strings.TrimSpace(string(content)) } +// defaultBackendURLFor derives the observability API endpoint from the LaunchDarkly +// base URI, so aiming the CLI at another instance takes the one flag that names the +// instance rather than two flags that have to agree. +// +// Only LaunchDarkly's own hosts are derived from. A base URI pointing at a local or +// proxied stack says nothing about where its observability API listens, so those keep +// the production default and --backend-url stays the way to say otherwise. +func defaultBackendURLFor(baseURI string) string { + parsed, err := url.Parse(strings.TrimSpace(baseURI)) + if err != nil { + return defaultBackendUrl + } + + host := parsed.Hostname() + if host != launchDarklyDomain && !strings.HasSuffix(host, "."+launchDarklyDomain) { + return defaultBackendUrl + } + return "https://" + observabilityAPIPrefix + host +} + // getSymbolUploadUrls returns one upload URL per requested key, in order. // // With skipExisting, a key whose bytes the backend already stores comes back empty @@ -712,7 +742,7 @@ func initFlags(cmd *cobra.Command) { cmd.Flags().String(basePathFlag, "", "An optional base path for the uploaded symbol files") _ = viper.BindPFlag(basePathFlag, cmd.Flags().Lookup(basePathFlag)) - cmd.Flags().String(backendUrlFlag, defaultBackendUrl, "An optional backend url for self-hosted deployments") + cmd.Flags().String(backendUrlFlag, "", fmt.Sprintf("An optional backend url for self-hosted deployments. Defaults to the observability API of whichever instance --%s names (%s for the default)", cliflags.BaseURIFlag, defaultBackendUrl)) _ = viper.BindPFlag(backendUrlFlag, cmd.Flags().Lookup(backendUrlFlag)) cmd.Flags().Bool(includeSourcesFlag, false, fmt.Sprintf("Also upload your source files so the errors page can show source context around native frames (%s and %s). Your source is stored in LaunchDarkly", typeAppleDSYM, typeAndroid)) diff --git a/cmd/symbols/upload_test.go b/cmd/symbols/upload_test.go index 04cc87be..6b0cf468 100644 --- a/cmd/symbols/upload_test.go +++ b/cmd/symbols/upload_test.go @@ -34,6 +34,28 @@ func TestNewUploadCmd(t *testing.T) { assert.Equal(t, []string{"true"}, cmd.Flags().Lookup("project").Annotations["required"]) } +// Naming the instance once, with --base-uri, is what should aim an upload at it: the +// observability API of every LaunchDarkly instance is named for that instance, so +// asking for both is asking for two flags that can disagree. +func TestDefaultBackendURLFor(t *testing.T) { + for name, tc := range map[string]struct{ baseURI, want string }{ + "production": {"https://app.launchdarkly.com", defaultBackendUrl}, + "staging": {"https://ld-stg.launchdarkly.com", "https://pri.observability.ld-stg.launchdarkly.com"}, + "trailing slash": {"https://ld-stg.launchdarkly.com/", "https://pri.observability.ld-stg.launchdarkly.com"}, + "surrounding whitespace": {" https://ld-stg.launchdarkly.com ", "https://pri.observability.ld-stg.launchdarkly.com"}, + "regional instance": {"https://app.eu.launchdarkly.com", "https://pri.observability.app.eu.launchdarkly.com"}, + + // Nothing about one of these says where an observability API listens, so the + // production default stands and --backend-url remains how to say otherwise. + "local stack": {"http://localhost:3000", defaultBackendUrl}, + "host that merely ends in the domain name": {"https://notlaunchdarkly.com", defaultBackendUrl}, + "unset": {"", defaultBackendUrl}, + "garbage": {"://", defaultBackendUrl}, + } { + assert.Equal(t, tc.want, defaultBackendURLFor(tc.baseURI), name) + } +} + func TestIsReactNativeUploadFile(t *testing.T) { // React Native iOS bundle + map. assert.True(t, isReactNativeUploadFile("main.jsbundle"))