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

feat: Avoid throwing errors for shared input/output IDs #4101

Merged
merged 29 commits into from
Dec 6, 2024

Conversation

gadenbuie
Copy link
Member

@gadenbuie gadenbuie commented Jul 15, 2024

Fixes #4100

Description

This PR refactors the Shiny Client Console to allow client error messages to be communicated via events in addition to errors.

The primary goal is to avoid throwing an error from the binding validity checks for shared input/output ID errors. In practice, there were two problems with the binding validity checks throwing errors:

  1. There are many cases where an app with shared IDs may still work, but by throwing an error we guarantee that the app breaks. Because we throw the error from Shiny's initialization code, we absolutely guarantee the app will not work, even if in practice the ID collision would not have caused problems.

  2. We were previously guarding some errors behind dev mode being enabled, meaning that turning on dev mode could break an app that otherwise works. IMHO, dev mode should add information by showing users additional context or information, but should not introduce large breaking changes into the app.

By allowing the client error console to receive messages via event, we can now have bindAll() report binding issues without throwing or breaking the app.

This refactor also improves the code organization and opens us up to future improvements in a few ways:

  • I consolidated the message display logic so that the client error console handles redirecting messages to the console when dev mode is not enabled.
  • A client message is currently assumed to be an error, but we can now easily expand the properties of a ShinyClientMessage in the future to add, for example, informational messages differentiated by a type prop.
  • The event handler approach opens the client error console to third-party use-cases via the shiny:client-message event.
Original

This PR was originally written as described below, now updated.

Note that we do still throw an error when we detect more than one of the same input IDs or output IDs (i.e. "debug" is used for two+ inputs or is used for two+ outputs). In theory, these apps could still work but would be much harder to reason about. More importantly, by still throwing in these cases, this PR only changes behavior in the narrow case of the shared input/output ID.

Note that after the Shiny Client Console was introduced (v1.8.0), we no longer threw client-side errors for 2+ outputs. We have never thrown for 2+ inputs. As such, this PR doesn't introduce new breaking changes and simply ensures that apps run the same in devmode as in "production". Enabling devmode() now simply turns on the additional UI affordance of showing the messages in the client console.

Example

The following app currently works as expected in "production" but breaks when devmode is enabled. With this PR, the app works in both modes and a message is shown either in the console or in the client error console when devmode is enabled.

library(shiny)
library(bslib)

options(shiny.devmode = TRUE)

ui <- page_fluid(
  actionButton("debug", "Debug"),
  # textInput("debug", "Other debug"),
  verbatimTextOutput("debug")
)

server <- function(input, output, session) {
  output$debug <- renderPrint({
    input$debug
  })
}

shinyApp(ui, server)

The problem can be seen in this shinylive.io app (as of 2024-07-15).

@gadenbuie gadenbuie requested a review from cpsievert July 15, 2024 13:58
gadenbuie and others added 3 commits July 15, 2024 10:15
It's otherwise hard to tell that the error is scrollable
Plus the scrolling is over the whole message rather than the part that overflows
@gadenbuie gadenbuie changed the title feat: Avoid throwing binding validity errors feat: Avoid throwing binding validity errors for shared input/output IDs Jul 15, 2024
@gadenbuie gadenbuie changed the title feat: Avoid throwing binding validity errors for shared input/output IDs feat: Avoid throwing errors for shared input/output IDs Jul 15, 2024
@gadenbuie gadenbuie requested a review from jcheng5 September 27, 2024 18:44
srcts/src/shiny/bind.ts Outdated Show resolved Hide resolved
srcts/src/shiny/bind.ts Outdated Show resolved Hide resolved
@gadenbuie gadenbuie added this to the Next Release milestone Nov 13, 2024
@gadenbuie
Copy link
Member Author

I've held off on merging this because doing so would introduce breaking changes for the case where 2+ inputs have the same ID. It would also go back to pre v1.8.0 behavior for duplicate output IDs.

Here is a short summary of behavior:

  1. Before: Some events throw errors in Shiny dev mode but not in production (dev mode disabled).
    • After: Errors are consistent; if devmode is enabled they show in the client console, otherwise in the browser console.
  • Before (v1.8.0): 2+ outputs throws an error, stopping the app. 2+ inputs does not throw an error but has inconsistent behavior.
    • After (v1.8.1): 2+ outputs and 2+ inputs throw an error in devmode only.
    • After (PR): Both 2+ outputs and 2+ inputs throw an error regardless of devmode.
  • Before (v1.8.0): Same ID used for input and output does not throw and generally works.
    • After (v1.8.1): Same ID used for input+output throws an errror in devmode, works without a message in prod.
    • After (PR): Console warning message is printed in client console (devmode) or browser console (prod)

As it stands in this PR, the biggest change would be that using "debug" for two inputs would become an app-stopping error. The alternative is to avoid throwing errors in production mode but with warnings in the console. This avoids obviously broken apps, but doesn't protect authors from subtle bugs from indeterminate behavior.

I think it might be better to avoid errors now, investigate if we can surface these warnings server-side as well, and then state an intention to elevate the warnings to errors in a future version of Shiny.

gadenbuie and others added 4 commits December 5, 2024 12:29
Brings dev mode in line with current "prod" behavior,
where errors aren't thrown for duplciates. In both cases
we still get console or client messages.
@gadenbuie
Copy link
Member Author

After discussion in person with @jcheng5 and @cpsievert, we decided not to have duplicate input/output IDs throw errors client side to avoid breaking apps that currently work.

All users are recommend to enable developer mode when developing Shiny apps locally, which can be done by calling devmode(TRUE) or by setting the shiny.devmode option in your ~/.Rprofile:

options(shiny.devmode = TRUE)

@gadenbuie gadenbuie requested a review from jcheng5 December 5, 2024 17:53
@jcheng5
Copy link
Member

jcheng5 commented Dec 6, 2024

I didn't have the brain cells to fully grok bind.ts but otherwise, things look fine.

Eventually I'd like us to seriously consider having the console UI pop up on all unhandled JS errors (window.addEventListener("error")), all unhandled promise rejections, and all calls to console.error. But not for this release obviously.

Comment on lines 168 to 171
return {
status: "error",
error: new ShinyClientError({
headline: "Duplicate input/output IDs found",
message: `The following ${
duplicateIds.size === 1 ? "ID was" : "IDs were"
} repeated:\n${duplicateIdMsg}`,
}),
error: new ShinyClientError({ headline, message }),
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems status: "error" is no longer possible/relevant for checkValidity? In that case, I'd be favor of removing it completely, or at least a healthy comment about how we imagine it being relevant in the future

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, I had left this because I didn't want to change the scope of what happens with checkValidity() too much. But I've refactored it now for clarity, which helps a lot and brings all of the logic into checkValidity().

Copy link
Collaborator

@cpsievert cpsievert left a comment

Choose a reason for hiding this comment

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

Nice, thanks!

@gadenbuie gadenbuie merged commit e5083f4 into main Dec 6, 2024
1 check passed
@gadenbuie gadenbuie deleted the feat/client-message-event branch December 6, 2024 21:00
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.

Shared input/output IDs shouldn't break apps even in devmode
3 participants