Compare commits

..

82 Commits

Author SHA1 Message Date
shikokuchuo
c4c91f62a1 Scope OTEL promise domain to startApp() setup and service loop
`local_otel_promise_domain()` binds the domain to the caller's frame,
which in `startApp()` exits before any request is served. A persistent
global install would leak into unrelated user promises between ticks.

Wrap the synchronous setup phase and each service iteration in
`with_otel_promise_domain()`. Callbacks are wrapped at registration
time, so promises created during `onStart`, handlers, and observers
stay instrumented when they fire. The domain is dormant between ticks,
so it stays out of user promises at the console.
2026-04-17 15:08:54 +01:00
shikokuchuo
88b4facb8f Move option resolution into .setupShinyApp()
Aligns `startApp()` with `runApp()` by setting `options(warn,
pool.scheduler)` before `as.shiny.appobj()` and passing `ops` through.
Folds the `findVal` precedence block into `.setupShinyApp()`; missingness
is checked in the caller's frame via a `caller = parent.frame()` default
arg, since `runApp()`/`startApp()` formals carry defaults.
2026-04-17 12:48:42 +01:00
shikokuchuo
957b50d3b6 Fix stale variable name in test comment 2026-04-17 12:27:52 +01:00
shikokuchuo
da50bf2249 Add startApp() to pkgdown.yml 2026-04-15 21:18:39 +01:00
shikokuchuo
72636ef4a0 Amend stopApp() docs 2026-04-10 15:28:07 +01:00
shikokuchuo
02a5e0b40f Separate out non-blocking into startApp() 2026-04-10 15:11:08 +01:00
Charlie Gao
0ff93d411f Apply suggestions from code review
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2026-03-20 10:31:07 +00:00
shikokuchuo
bd250962e4 Amend currently running message 2026-03-20 10:29:37 +00:00
shikokuchuo
3a0e8627a4 Have handle$stop() method handle setting of stopped flag 2026-03-20 10:26:21 +00:00
shikokuchuo
b813adec56 Move generation increment before the blocking/non-blocking branch 2026-03-18 10:41:56 +00:00
shikokuchuo
f29fa65af9 Use generation invalidation for loops 2026-03-17 22:49:37 +00:00
shikokuchuo
36e7a330d6 Update news 2026-03-17 22:32:51 +00:00
shikokuchuo
6d984266f9 devtools::document() (GitHub Actions) 2026-03-13 18:54:05 +00:00
shikokuchuo
c27c186c0f Make non-default for LLMs 2026-03-13 18:51:09 +00:00
Charlie Gao
2907e83c42 Merge branch 'main' into non-blocking 2026-03-05 14:09:04 +00:00
Carson Sievert
75a63716e5 Increment version number to 1.13.0.9000 (#4361)
* Increment version number to 1.13.0.9000

* `npm run build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2026-02-24 16:39:52 -06:00
Carson Sievert
b240b0b868 v1.13.0 release candidate (#4360)
* Increment version number to 1.13.0

* Fix broken URL in NEWS

* Reorder NEWS bullets

* `npm run build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2026-02-24 14:58:46 -06:00
Garrick Aden-Buie
3c18aca49b fix: add stack trace fences to hide internal render pipeline frames (#4358)
* test: add failing tests for render* stack trace fence coverage

Add tests for renderPlot, renderPrint, renderText, renderUI,
renderTable, and renderImage verifying that internal rendering
pipeline frames are hidden by stack trace fences.

All 6 tests fail, revealing that:
- All render functions leak `renderFunc` through the fences
- renderPlot additionally leaks `drawPlot`, `drawReactive`
- renderPrint additionally leaks `with_promise_domain`

Relates to #4357

* refactor: move stack trace test helpers to helper-stacks.R

Move extractStackTrace, cleanLocs, dumpTests, and
captureFilteredRenderTrace into helper-stacks.R so they are
available to all test files. Rename causeRenderError to
captureFilteredRenderTrace.

* fix: add stack trace fences to hide internal render pipeline frames

Add ..stacktraceoff../..stacktraceon.. fence pairs so that internal
rendering pipeline frames (renderFunc, hybrid_chain, drawPlot, etc.)
are hidden from filtered stack traces in the debugger.

- markRenderFunction: wrap renderFunc() call with ..stacktraceoff..
- createRenderFunction: wrap func() with ..stacktraceon.. to restore
  visibility for user code
- renderPrint: wrap func() with ..stacktraceon.. inside promise domain

For renderPlot, the existing ..stacktraceon from installExprFunction
is sufficient once the outer ..stacktraceoff.. is in place.

Fixes #4357

* fix: Legacy datatable stack traces

* chore: Add news bullet

* chore: Just normalize path in the place that needs it

* chore: don't normalize file path

* fix: Handle srcfilealias in stack traces and telemetry

Following commit 272dda27e, which normalized paths only in the #line
directive, sourceUTF8() now creates srcfilealias objects for user code.
This broke code that assumed only package code had srcfile$original.

## How the new approach works

When sourceUTF8() wraps code with a #line directive:

```r
file <- 'app.R'  # Keep original path (relative/symlink/as-typed)
lines <- c(
  '..stacktraceon..({',
  sprintf('#line 1 "%s"', normalizePath(file, ...)),  # Normalize HERE
  readLines(file),
  '})'
)
src <- srcfilecopy(file, lines, isFile = TRUE)  # Uses original path
expr <- parse(text = lines, srcfile = src)
```

The parser sees the #line path differs from srcfilecopy's path, so it
creates a srcfilealias with:
- srcfile$filename = absolute path (from #line, for source refs)
- srcfile$original$filename = original path (from srcfilecopy)

This gives us both: accurate source references + user-friendly paths.

## Changes made

1. Add getSrcfileFilename() helper
   - Prefers $original$filename (user-typed path) when available
   - Falls back to $filename (absolute) for old-style srcfile objects
   - Ensures stack traces show "app.R#10" not "/abs/path/app.R#10"

2. Add isPackageFile() helper
   - Checks if absolute path is under .libPaths()
   - More reliable than checking for $original presence

3. Fix getCallCategories()
   - Now uses isPackageFile() instead of checking $original
   - User code properly categorized as "user" (bold blue in traces)
   - Package code properly categorized as "pkg" (de-emphasized)

4. Update getLocs() and otel_srcref_attributes()
   - Use getSrcfileFilename() to show user-friendly paths

## Benefits

- Stack traces preserve relative paths and symlinks as users typed them
- User vs package code still correctly distinguished
- Better IDE integration (paths match what user entered)
- Telemetry contains meaningful file paths

* fix: Avoid using startsWith()

* fix: Use reverse clamped cumsum for stack trace fence filtering

Replace the forward cumulative sum in stripStackTraces() with a reverse
clamped cumulative sum so that an unmatched `..stacktraceoff..` (one
with no corresponding inner `..stacktraceon..`) is a no-op. This fixes
a regression where markRenderFunction-only callers (e.g. htmlwidgets)
had their user frames hidden when called outside a reactive domain.

The new algorithm concatenates all trace segments into a single vector,
performs vectorized fence scoring, and computes visibility via the
identity: clamped_cumsum = cumsum - pmin(0, cummin(cumsum)).

Fixes #4357

* refactor: Extract skip_if_shiny_otel_tracer_is_enabled() helper

* fix: Handle srcfilealias in reactive auto-labeling

The normalizePath() in sourceUTF8() causes R to create srcfilealias
objects whose $lines is NULL, breaking rassignSrcrefToLabel() and
rexprSrcrefToLabel(). Add getSrcfileLines() helper (alongside
getSrcfileFilename()) to resolve lines from the original srcfilecopy
using srcref[7] for the correct line number.

* fix: Enforce path-boundary check in isPackageFile()

The prefix-only matching in isPackageFile() could misclassify paths
like "/usr/lib/Rcpp/..." as inside "/usr/lib/R". Normalize library
paths with a trailing slash before comparison to ensure proper
path-boundary matching.

* fix: Prefer original filename only for non-package srcfilealias

When a package is installed with keep.source.pkgs = TRUE, the
srcfilecopy original filename may point to a collated build-time path.
For package files (under .libPaths()), keep srcfile$filename to avoid
regressing stack traces and telemetry with install-time paths.

* Update R/conditions.R

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2026-02-18 17:01:33 -06:00
shikokuchuo
45985690b2 Apply review changes from @cpsievert and @schloerke 2026-02-18 18:39:07 +00:00
shikokuchuo
ce11abe46d Merge branch 'main' into non-blocking 2026-02-18 12:19:35 +00:00
shikokuchuo
7e8903f754 Consistent cleanup order 2026-02-18 12:05:10 +00:00
shikokuchuo
664cbe2858 Move cleanup func creation 2026-02-18 12:00:06 +00:00
shikokuchuo
63af3649c8 Minimize changes to on.exit() handlers 2026-02-18 11:46:34 +00:00
Garrick Aden-Buie
9b78be1106 feat: Maybe annotate source for Ark (#4352)
* feat: Maybe annotate source for Ark

* fix: Use pre-parse approach to inject `..stacktraceon..`

* chore: Address code review feedback

* chore: Add news bullet

---------

Co-authored-by: Lionel Henry <lionel.hry@proton.me>
2026-02-17 12:22:07 -05:00
shikokuchuo
3cb928e894 Simplify serviceAsync() 2026-02-17 12:40:47 +00:00
shikokuchuo
8e63d08d8a Make handle$stop() idempotent 2026-02-17 12:21:22 +00:00
Charlie Gao
1db26f60af Merge branch 'main' into non-blocking 2026-02-17 12:09:31 +00:00
shikokuchuo
1432920a7e Refactor pass 2026-02-17 11:50:11 +00:00
shikokuchuo
3882d1e4c3 Update news and docs 2026-02-17 11:08:52 +00:00
shikokuchuo
532c17081a Auto-stop upon new runApp() 2026-02-17 10:54:04 +00:00
shikokuchuo
329bc979c6 Default to non-blocking for LLMs 2026-02-17 10:39:14 +00:00
E Nelson
3a130b2015 fix: Update to increase whitespace (#4356)
* Update to increase whitespace

* Update scss again
2026-02-14 09:28:33 -06:00
Carson Sievert
27ddc696dc chore: avoid testing elapsed time on CRAN (#4351)
* chore: avoid testing elapsed time on CRAN

* chore: update tabPanel snapshot for disabled attribute on empty tabs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 18:34:39 -06:00
shikokuchuo
0456847883 Add documentation about running the later loop 2026-02-06 21:47:50 +00:00
shikokuchuo
6bbe29a390 Fix cleanup step and add regression test 2026-02-06 21:24:45 +00:00
shikokuchuo
c8bfa93747 Consolidate two on.exit() clauses 2026-02-06 20:31:45 +00:00
Charlie Gao
27134d9c66 Merge branch 'main' into non-blocking 2026-02-06 19:20:15 +00:00
E Nelson
4d787c767c Update to remove whitespace and add padding when necessary (#4350)
* Update to remove whitespace and add padding when necessary

* Cleanup

* `npm run build` (GitHub Actions)

* Consolidate rules

* Updated action button

* `npm run build` (GitHub Actions)

---------

Co-authored-by: elnelson575 <elnelson575@users.noreply.github.com>
2026-02-06 09:49:06 -06:00
shikokuchuo
48540283a4 Use combined error message 2026-02-05 15:34:43 +00:00
shikokuchuo
49b76badcc Rename internal function 2026-02-05 15:07:40 +00:00
shikokuchuo
935de77aee Cleanup pass 2026-02-05 14:42:16 +00:00
shikokuchuo
8b53c6d2fd Silence tests 2026-02-05 14:03:59 +00:00
shikokuchuo
3ccbad7a70 Tighten later loop for performance 2026-02-05 13:53:38 +00:00
shikokuchuo
13812b45a7 Review internal methods 2026-02-05 12:45:03 +00:00
shikokuchuo
08680d9566 Review user interface 2026-02-05 11:40:52 +00:00
shikokuchuo
620e5a277b Update unrelated tabPanel test snapshot 2026-02-04 19:41:06 +00:00
shikokuchuo
bb26c0f4d3 Non-blocking concept 2026-02-04 16:28:06 +00:00
Garrick Aden-Buie
e161f2e4a8 fix: Provide context around SHINY_PORT triggering Shiny Server warning (#4345)
* fix: Provide context around SHINY_PORT triggering Shiny Server warning

* `npm run build` (GitHub Actions)

* chore: Add comma

Co-authored-by: Barret Schloerke <barret@posit.co>

---------

Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
Co-authored-by: Barret Schloerke <barret@posit.co>
2026-01-12 11:23:25 -05:00
Copilot
ca259ab0f1 ci: Rebuild npm assets and skip tests for unavailable Suggests packages (#4344)
Co-authored-by: schloerke <93231+schloerke@users.noreply.github.com>
2026-01-12 10:37:48 -05:00
Aditya bansal
9e9a3bf80b R 4.2 / Cairo testing changes reverted (#4342) 2026-01-12 09:21:54 -05:00
Karan
07af5f91c8 chore(license): Change license from GPL-3 to MIT (#4339)
* Change license from GPL-3 to MIT

Updated the project license from GPL-3 to MIT in DESCRIPTION, LICENSE, LICENSE.md, README.md, and package.json. Added LICENSE.md with the MIT license text and updated .Rbuildignore to exclude LICENSE.md from builds.

* `npm run build` (GitHub Actions)

* Update LICENSE and add LICENSE.note

Replaced the LICENSE file content with a summary including year and copyright holder. Moved detailed third-party license information to a new LICENSE.note file.

* Remove R check log file

Deleted the ..Rcheck/00check.log file, likely to clean up generated or temporary files from the repository.
2025-12-16 17:51:22 -06:00
Barret Schloerke
fda6a9fede chore(assets): Update asset versions (#4337) 2025-12-11 11:56:42 -05:00
Barret Schloerke
d2245a2e34 Increment version number to 1.12.1.9000 2025-12-09 16:29:27 -05:00
Barret Schloerke
a12a8130b8 v1.12.1 (#4329) 2025-12-09 16:26:52 -05:00
Barret Schloerke
b436d2a96d Clarify OTel collection level usage in docs (#4335)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2025-12-08 15:31:57 -05:00
Barret Schloerke
05b0f270c4 fix(otel): ExtendedTask's otel enabled status set during init (#4334) 2025-12-08 14:55:59 -05:00
Barret Schloerke
f24f71e4e0 feat(otel): Add withOtelCollect() and localOtelCollect() (#4333) 2025-12-08 14:30:40 -05:00
Barret Schloerke
63a00f775f fix(otel): Duplicate otel code attribute keys using both deprecated and preferred names (#4325) 2025-12-03 16:37:20 -05:00
Barret Schloerke
5a946caf35 Skip timer tests on CRAN and fix empty vector comparison (#4327) 2025-12-03 16:29:17 -05:00
Barret Schloerke
16c016a171 Increment version number to 1.12.0.9000 2025-12-03 15:50:36 -05:00
Barret Schloerke
284af65534 Update .Rbuildignore 2025-12-03 15:50:27 -05:00
Barret Schloerke
b5da7868fa v1.12.0 (#4312) 2025-12-03 15:48:49 -05:00
ismirsehregal
c8a41aa834 Update dateYMD (#4318)
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2025-12-01 10:25:28 -05:00
Copilot
390f6d3b95 chore(otel): Rename shiny.otel.bind to shiny.otel.collect (#4321)
Co-authored-by: Barret Schloerke <barret@posit.co>
2025-11-25 16:36:56 -05:00
Barret Schloerke
9a2140cd19 chore(test): Fix stacks test with R 4.5.2 (#4322) 2025-11-25 16:31:53 -05:00
Barret Schloerke
e3cf4fb089 refactor(otel): Cache tracer and logger on init and on demand (#4315)
Co-authored-by: Charlie Gao <53399081+shikokuchuo@users.noreply.github.com>
2025-11-11 08:44:57 -05:00
Barret Schloerke
472a1cdba1 refactor(otel): Refactor internal method names (#4313) 2025-11-06 11:20:37 -05:00
Barret Schloerke
b56c275364 feat(otel): Enhanced OpenTelemetry support (#4300) 2025-10-28 14:01:50 -04:00
Garrick Aden-Buie
592e825a0f feat: Replace client-side markdown rendering with server-side in showcase mode (#4306)
* feat: Replace client-side markdown rendering with server-side in showcase mode

* `devtools::document()` (GitHub Actions)

* chore: callout rendering differences may happen
2025-10-22 09:34:20 -04:00
Barret Schloerke
50a140c580 ci(test): Disable installation of Cairo for unit tests (#4301) 2025-10-15 16:24:22 -04:00
Barret Schloerke
48d255a235 feat: Add {otel} support (#4269) 2025-10-14 15:40:36 -04:00
Barret Schloerke
a01fcc5194 chore(check): Fix minor check warnings (#4299) 2025-10-10 16:30:47 -04:00
Winston Chang
b6e9e9d216 Update package.json for publishing (#4284)
* Update package.json for publishing

* Use custom readme file for npm

* Update repository URLs

* Script cleanup

* Use name @posit/shiny
2025-09-09 16:37:06 -05:00
Barret Schloerke
5ddb99a5b4 update docs (#4285) 2025-09-05 13:52:30 -04:00
Carson Sievert
f981ed6363 feat: add shinyRemoveButton selectize.js option (for py-shiny) (#4276)
* fix: supply and retain default selectize.js plugins (for py-shiny)

* `npm run build` (GitHub Actions)

* Move more in a 'remove_button' attribute direction

* `npm run build` (GitHub Actions)

* Move to a JSON-only approach

* `npm run build` (GitHub Actions)

* Drop sticky update logic by always sending 'missing' value and resolving client-side

* Cleanup

* Don't mutate options; better typing pattern

* `npm run build` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-08-28 15:49:25 -05:00
Carson Sievert
6d6b0ea6f9 fix: add CSS workaround to hide all but the first selectize remove button (#4275)
* fix: add CSS workaround to hide all but the first selectize remove button

* Update news

* Apply suggestions from code review
2025-08-21 16:48:34 -05:00
Barret Schloerke
0e355ed25c preemptively run revdepcheck. 5 failures. 2025-08-18 09:10:19 -04:00
Barret Schloerke
80a9ff470c fix(scss): Remove updated scss files and restore R sass compilation (#4272)
* Revert changes in 8861645d99 from #4252

We compile from R sass. So we should be partial to R's compiler, no the latest in npm

* Restore original compile script (with message) that built using R-sass, not npm

* Add step in build command to update the sass output

* run new command
2025-08-14 16:03:48 -04:00
Barret Schloerke
ead0abcd62 bug(check): Update .Rbuildignore with new and removed files (#4270) 2025-08-04 23:17:45 -04:00
Barret Schloerke
7dcb54bc7e build(js): Migrate from yarn to npm (#4252) 2025-08-04 16:07:39 -04:00
Colin Gillespie
ae82850e1f docs: Update R supported versions (#4256) 2025-08-04 10:37:48 -04:00
Daniel Chen
0610d756a8 docs: update srcts/readme.md to shiny 1.11.1 (#4259) 2025-08-04 09:50:06 -04:00
258 changed files with 17140 additions and 15918 deletions

View File

@@ -21,19 +21,17 @@
^TODO-promises.md$
^manualtests$
^\.github$
^\.yarn$
^\.vscode$
^\.madgerc$
^\.prettierrc\.yml$
^jest\.config\.js$
^package\.json$
^tsconfig\.json$
^yarn\.lock$
^package-lock\.json$
^node_modules$
^coverage$
^.ignore$
^\.browserslistrc$
^\.eslintrc\.yml$
^\.yarnrc\.yml$
^eslint\.config\.mjs$
^_dev$
^.claude$
^README-npm\.md$
^CRAN-SUBMISSION$
^LICENSE\.md$

View File

@@ -1,8 +0,0 @@
# Browsers that we support
last 2 versions
not dead
> 0.2%
# > 1%
Firefox ESR
phantomjs 2.1
IE 11 # sorry

View File

@@ -1,110 +0,0 @@
root: true
env:
browser: true
es6: true
extends:
- 'eslint:recommended'
- 'plugin:@typescript-eslint/recommended'
- 'plugin:jest/recommended'
- 'plugin:prettier/recommended'
- 'plugin:jest-dom/recommended'
globals:
Atomics: readonly
SharedArrayBuffer: readonly
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2018
sourceType: module
project:
- './tsconfig.json'
ignorePatterns: # mirrors tsconfig.json's exclude
- '**/__tests__'
- '**/*.d.ts'
plugins:
- '@typescript-eslint'
- prettier
- jest-dom
- unicorn
rules:
"@typescript-eslint/explicit-function-return-type":
- off
"@typescript-eslint/no-explicit-any":
- off
"@typescript-eslint/explicit-module-boundary-types":
- error
default-case:
- error
linebreak-style:
- error
- unix
quotes:
- error
- double
- avoid-escape
semi:
- error
- always
dot-location:
- error
- property
camelcase:
# - error
- "off"
unicorn/filename-case:
- error
- case: camelCase
"@typescript-eslint/array-type":
- error
- default: array-simple
readonly: array-simple
"@typescript-eslint/consistent-indexed-object-style":
- error
- index-signature
"@typescript-eslint/sort-type-union-intersection-members":
- error
"@typescript-eslint/consistent-type-imports":
- error
"@typescript-eslint/no-floating-promises":
- error
"@typescript-eslint/naming-convention":
- error
- selector: default
format: [camelCase]
- selector: method
modifiers: [private]
format: [camelCase]
leadingUnderscore: require
- selector: method
modifiers: [protected]
format: [camelCase]
leadingUnderscore: require
- selector: variable
format: [camelCase]
trailingUnderscore: forbid
leadingUnderscore: forbid
- selector: parameter
format: [camelCase]
trailingUnderscore: allow
leadingUnderscore: forbid
- selector: [enum, enumMember]
format: [PascalCase]
- selector: typeLike
format: [PascalCase]
custom:
regex: "(t|T)ype$"
match: false

View File

@@ -5,8 +5,8 @@ echo "Updating package.json version to match DESCRIPTION Version"
Rscript ./tools/updatePackageJsonVersion.R
if [ -n "$(git status --porcelain package.json)" ]
then
echo "package.json has changed after running ./tools/updatePackageJsonVersion.R. Re-running 'yarn build'"
yarn build
echo "package.json has changed after running ./tools/updatePackageJsonVersion.R. Re-running 'npm run build'"
npm run build
git add ./inst package.json && git commit -m 'Sync package version (GitHub Actions)' || echo "No package version to commit"
else
echo "No package version difference detected; package.json is current."

View File

@@ -6,9 +6,9 @@ on:
push:
branches: [main, rc-**]
pull_request:
branches: [main]
branches:
schedule:
- cron: '0 5 * * 1' # every monday
- cron: "0 5 * * 1" # every monday
name: Package checks
@@ -17,7 +17,5 @@ jobs:
uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1
routine:
uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1
with:
node-version: "14.x"
R-CMD-check:
uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1

12
.gitignore vendored
View File

@@ -9,20 +9,16 @@
shinyapps/
README.html
.*.Rnb.cached
tools/yarn-error.log
/_dev/
.sass_cache_keys
# TypeScript / yarn
# TypeScript
/node_modules/
.cache
.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
coverage/
madge.svg
# GHA remotes installation
.github/r-depends.rds
.claude/settings.local.json

View File

@@ -1,13 +1,12 @@
{
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"prettier.prettierPath": "./node_modules/prettier",
"typescript.enablePromptUseWorkspaceTsdk": true,
"[r]": {
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"editor.formatOnSave": false,
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +0,0 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: "https://github.com/mskelton/yarn-plugin-outdated/raw/main/bundles/@yarnpkg/plugin-outdated.js"
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.3.cjs
checksumBehavior: update

View File

@@ -1,7 +1,7 @@
Type: Package
Package: shiny
Title: Web Application Framework for R
Version: 1.11.1.9000
Version: 1.13.0.9000
Authors@R: c(
person("Winston", "Chang", , "winston@posit.co", role = "aut",
comment = c(ORCID = "0000-0002-1576-2126")),
@@ -60,10 +60,6 @@ Authors@R: c(
comment = "Javascript strftime library"),
person(, "SpryMedia Limited", role = c("ctb", "cph"),
comment = "DataTables library"),
person("John", "Fraser", role = c("ctb", "cph"),
comment = "showdown.js library"),
person("John", "Gruber", role = c("ctb", "cph"),
comment = "showdown.js library"),
person("Ivan", "Sagalaev", role = c("ctb", "cph"),
comment = "highlight.js library"),
person("R Core Team", role = c("ctb", "cph"),
@@ -73,17 +69,17 @@ Description: Makes it incredibly easy to build interactive web
applications with R. Automatic "reactive" binding between inputs and
outputs and extensive prebuilt widgets make it possible to build
beautiful, responsive, and powerful applications with minimal effort.
License: GPL-3 | file LICENSE
License: MIT + file LICENSE
URL: https://shiny.posit.co/, https://github.com/rstudio/shiny
BugReports: https://github.com/rstudio/shiny/issues
Depends:
methods,
R (>= 3.0.2)
R (>= 3.1.2)
Imports:
bslib (>= 0.6.0),
cachem (>= 1.1.0),
cli,
commonmark (>= 1.7),
commonmark (>= 2.0.0),
fastmap (>= 1.1.1),
fontawesome (>= 0.4.0),
glue (>= 1.3.2),
@@ -94,7 +90,8 @@ Imports:
later (>= 1.0.0),
lifecycle (>= 0.2.0),
mime (>= 0.3),
promises (>= 1.3.2),
otel,
promises (>= 1.5.0),
R6 (>= 2.0),
rlang (>= 0.4.10),
sourcetools,
@@ -114,6 +111,7 @@ Suggests:
magrittr,
markdown,
mirai,
otelsdk (>= 0.2.0),
ragg,
reactlog (>= 1.0.0),
rmarkdown,
@@ -126,8 +124,9 @@ Config/Needs/check: shinytest2
Config/testthat/edition: 3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
RoxygenNote: 7.3.3
Collate:
'app-handle.R'
'globals.R'
'app-state.R'
'app_template.R'
@@ -185,6 +184,15 @@ Collate:
'modal.R'
'modules.R'
'notifications.R'
'otel-attr-srcref.R'
'otel-collect.R'
'otel-enable.R'
'otel-error.R'
'otel-label.R'
'otel-reactive-update.R'
'otel-session.R'
'otel-shiny.R'
'otel-with.R'
'priorityqueue.R'
'progress.R'
'react.R'

1016
LICENSE

File diff suppressed because it is too large Load Diff

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2025 shiny authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1011
LICENSE.note Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -165,6 +165,7 @@ export(isTruthy)
export(isolate)
export(key_missing)
export(loadSupport)
export(localOtelCollect)
export(mainPanel)
export(makeReactiveBinding)
export(markRenderFunction)
@@ -275,6 +276,7 @@ export(snapshotPreprocessInput)
export(snapshotPreprocessOutput)
export(span)
export(splitLayout)
export(startApp)
export(stopApp)
export(strong)
export(submitButton)
@@ -329,6 +331,7 @@ export(verticalLayout)
export(wellPanel)
export(withLogErrors)
export(withMathJax)
export(withOtelCollect)
export(withProgress)
export(withReactiveDomain)
export(withTags)
@@ -387,10 +390,13 @@ importFrom(lifecycle,is_present)
importFrom(promises,"%...!%")
importFrom(promises,"%...>%")
importFrom(promises,as.promise)
importFrom(promises,hybrid_then)
importFrom(promises,is.promise)
importFrom(promises,is.promising)
importFrom(promises,promise)
importFrom(promises,new_promise_domain)
importFrom(promises,promise_reject)
importFrom(promises,promise_resolve)
importFrom(promises,with_promise_domain)
importFrom(rlang,"%||%")
importFrom(rlang,"fn_body<-")
importFrom(rlang,"fn_fmls<-")

148
NEWS.md
View File

@@ -2,15 +2,151 @@
## New features
* The `icon` argument of `updateActionButton()`/`updateActionLink()` nows allows values other than `shiny::icon()` (e.g., `fontawesome::fa()`, `bsicons::bs_icon()`, etc). (#4249)
* New `startApp()` runs a Shiny app in non-blocking mode, returning a
`ShinyAppHandle` object with `stop()`, `status()`, `url()`, and `result()`
methods. When a new app is started, any previously running non-blocking app
is automatically stopped.
## Bug fixes
# shiny 1.13.0
* `updateActionButton()`/`updateActionLink()` now correctly renders HTML content passed to the `label` argument. (#4249)
## New features
## Changes
* Shiny now supports interactive breakpoints when used with Ark (e.g. in Positron). (#4352)
* The return value of `actionButton()`/`actionLink()` changed slightly: `label` and `icon` are wrapped in an additional HTML container element. This allows for: 1. `updateActionButton()`/`updateActionLink()` to distinguish between the `label` and `icon` when making updates and 2. spacing between `label` and `icon` to be more easily customized via CSS.
## Bug fixes and minor improvements
* Stack traces from render functions (e.g., `renderPlot()`, `renderDataTable()`) now hide internal Shiny rendering pipeline frames, making error messages cleaner and more focused on user code. (#4358)
* Fixed an issue with `actionLink()` that extended the link underline to whitespace around the text. (#4348)
# shiny 1.12.1
## New features
* `withOtelCollect()` and `localOtelCollect()` temporarily control
OpenTelemetry collection levels during reactive expression creation,
allowing you to enable or disable telemetry collection for specific modules
or sections of code. (#4333)
## Bug fixes and minor improvements
* OpenTelemetry code attributes now include both the preferred attribute names
(`code.file.path`, `code.line.number`, `code.column.number`) and the
deprecated names (`code.filepath`, `code.lineno`, `code.column`) to follow
OpenTelemetry semantic conventions while maintaining backward compatibility.
The deprecated names will be removed in a future release after Logfire
supports the preferred names. (#4325)
* `ExtendedTask` now captures the OpenTelemetry recording state at
initialization time rather than at invocation time, ensuring consistent span
recording behavior regardless of runtime configuration changes. (#4334)
* Timer tests are now skipped on CRAN. (#4327)
# shiny 1.12.0
## OpenTelemetry support
* Shiny now supports [OpenTelemetry](https://opentelemetry.io/) via
[`{otel}`](https://otel.r-lib.org/index.html). By default, if
`otel::is_tracing_enabled()` returns `TRUE`, then `{shiny}` records all
OpenTelemetry spans. See [`{otelsdk}`'s Collecting Telemetry
Data](https://otelsdk.r-lib.org/reference/collecting.html) for more details
on configuring OpenTelemetry. (#4269, #4300)
* Supported values for `options(shiny.otel.collect)` (or
`Sys.getenv("SHINY_OTEL_COLLECT")`):
* `"none"` - No Shiny OpenTelemetry tracing.
* `"session"` - Adds session start/end spans.
* `"reactive_update"` - Spans for any synchronous/asynchronous reactive
update. (Includes `"session"` features).
* `"reactivity"` - Spans for all reactive expressions. (Includes
`"reactive_update"` features).
* `"all"` [default] - All Shiny OpenTelemetry tracing. Currently equivalent
to `"reactivity"`.
* OpenTelemetry spans are recorded for:
* `session_start`: Wraps the calling of the `server()` function. Also
contains HTTP request within the attributes.
* `session_end`: Wraps the calling of the `onSessionEnded()` handlers.
* `reactive_update`: Signals the start of when Shiny knows something is to
be calculated. This span ends when there are no more reactive updates
(promises or synchronous) to be calculated.
* `reactive`, `observe`, `output`: Captures the calculation (including any
async promise chains) of a reactive expression (`reactive()`), an observer
(`observe()`), or an output render function (`render*()`).
* `reactive debounce`, `reactive throttle`: Captures the calculation
(including any async promise chains) of a `debounce()`d or `throttle()`d
reactive expression.
* `reactiveFileReader`, `reactivePoll`: Captures the calculation
(including any async promise chains) of a `reactiveFileReader()` or
`reactivePoll()`.
* `ExtendedTask`: Captures the calculation (including any async promise
chains) of an `ExtendedTask`.
* OpenTelemetry Logs are recorded for:
* `Set reactiveVal <name>` - When a `reactiveVal()` is set
* `Set reactiveValues <name>$<key>` - When a `reactiveValues()` element is
set
* Fatal or unhandled errors - When an error occurs that causes the session
to end, or when an unhandled error occurs in a reactive context. Contains
the error within the attributes. To unsanitize the error message being
collected, set `options(shiny.otel.sanitize.errors = FALSE)`.
* `Set ExtendedTask <name> <value>` - When an `ExtendedTask`'s respective
reactive value (e.g., `status`, `value`, and `error`) is set.
* `<ExtendedTask name> add to queue` - When an `ExtendedTask` is added to
the task queue.
* All OpenTelemetry logs and spans will contain a `session.id` attribute
containing the active session ID.
## New features
* `updateActionButton()` and `updateActionLink()` now accept values other than
`shiny::icon()` for the `icon` argument (e.g., `fontawesome::fa()`,
`bsicons::bs_icon()`, etc). (#4249)
## Bug fixes and minor improvements
* Showcase mode now uses server-side markdown rendering with the
`{commonmark}` package, providing support for GitHub Flavored Markdown
features (tables, strikethrough, autolinks, task lists). While most existing
README.md files should continue to work as expected, some minor rendering
differences may occur due to the change in markdown processor. (#4202,
#4201)
* `debounce()`, `reactiveFileReader()`, `reactivePoll()`, `reactiveValues()`,
and `throttle()` now attempt to retrieve the assigned name for the default
label if the srcref is available. If a value cannot easily be produced, a
default label is used instead. (#4269, #4300)
* The default label for items described below will now attempt to retrieve the
assigned name if the srcref is available. If a value can not easily be
produced, a default label will be used instead. This should improve the
OpenTelemetry span labels and the reactlog experience. (#4269, #4300)
* `reactiveValues()`, `reactivePoll()`, `reactiveFileReader()`, `debounce()`,
`throttle()`, `observe()`
* Combinations of `bindEvent()` and `reactive()` / `observe()`
* Combination of `bindCache()` and `reactive()`
* `updateActionButton()` and `updateActionLink()` now correctly render HTML
content passed to the `label` argument. (#4249)
* `updateSelectizeInput()` no longer creates multiple remove buttons when
`options = list(plugins="remove_button")` is used. (#4275)
* `dateRangeInput()`/`updateDateRangeInput()` now correctly considers the time
zones of date-time objects (POSIXct) passed to the `start`, `end`, `min` and
`max` arguments. (thanks @ismirsehregal, #4318)
## Breaking changes
* The return value of `actionButton()` and `actionLink()` now wraps `label`
and `icon` in an additional HTML container element. This allows
`updateActionButton()` and `updateActionLink()` to distinguish between the
`label` and `icon` when making updates, and allows spacing between `label`
and `icon` to be more easily customized via CSS.
# shiny 1.11.1
@@ -682,7 +818,7 @@ This release features plot caching, an important new tool for improving performa
### Minor new features and improvements
* Upgrade FontAwesome from 4.7.0 to 5.3.1 and made `icon` tags browsable, which means they will display in a web browser or RStudio viewer by default (#2186). Note that if your application or library depends on FontAwesome directly using custom CSS, you may need to make some or all of the changes recommended in [Upgrade from Version 4](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4). Font Awesome icons can also now be used in static R Markdown documents.
* Upgrade FontAwesome from 4.7.0 to 5.3.1 and made `icon` tags browsable, which means they will display in a web browser or RStudio viewer by default (#2186). Note that if your application or library depends on FontAwesome directly using custom CSS, you may need to make some or all of the changes recommended in [Upgrade from Version 4](https://docs-v5.fontawesome.com/web/setup/upgrade-from-v4). Font Awesome icons can also now be used in static R Markdown documents.
* Address #174: Added `datesdisabled` and `daysofweekdisabled` as new parameters to `dateInput()`. This resolves #174 and exposes the underlying arguments of [Bootstrap Datepicker](http://bootstrap-datepicker.readthedocs.io/en/latest/options.html#datesdisabled). `datesdisabled` expects a character vector with values in `yyyy/mm/dd` format and `daysofweekdisabled` expects an integer vector with day interger ids (Sunday=0, Saturday=6). The default value for both is `NULL`, which leaves all days selectable. Thanks, @nathancday! (#2147)

73
R/app-handle.R Normal file
View File

@@ -0,0 +1,73 @@
# Handle returned by startApp()
ShinyAppHandle <- R6::R6Class("ShinyAppHandle",
cloneable = FALSE,
public = list(
initialize = function(appUrl, cleanupFn) {
private$appUrl <- appUrl
private$cleanupFn <- cleanupFn
reg.finalizer(self, function(e) {
tryCatch(e$stop(), error = function(cnd) NULL)
}, onexit = TRUE)
},
stop = function() {
if (self$status() != "running") {
return(invisible(self))
}
private$stopped <- TRUE
private$captureResult()
private$cleanupFn()
private$cleanupFn <- NULL
invisible(self)
},
url = function() private$appUrl,
status = function() {
if (!private$stopped) {
"running"
} else if (!is.null(private$resultError)) {
"error"
} else {
"success"
}
},
result = function() {
if (self$status() == "running") {
stop("App is still running. Use status() to check if the app has stopped.")
}
if (!is.null(private$resultError)) {
stop(private$resultError)
}
private$resultValue
},
print = function(...) {
cat("Shiny app handle\n")
cat(" URL: ", private$appUrl, "\n", sep = "")
cat(" Status:", self$status(), "\n")
invisible(self)
}
),
private = list(
appUrl = NULL,
cleanupFn = NULL,
# Whether this handle has been stopped. Distinct from .globals$stopped
# which tracks whether a stop was requested (set by stopApp() or stop()).
stopped = FALSE,
resultValue = NULL,
resultError = NULL,
captureResult = function() {
if (isTRUE(.globals$reterror)) {
private$resultError <- .globals$retval
} else if (!is.null(.globals$retval)) {
private$resultValue <- .globals$retval$value
}
}
)
)

View File

@@ -478,7 +478,12 @@ bindCache.default <- function(x, ...) {
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
check_dots_unnamed()
label <- exprToLabel(substitute(key), "cachedReactive")
call_srcref <- get_call_srcref(-1)
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = exprToLabel(substitute(x), "cachedReactive")
)
domain <- reactive_get_domain(x)
# Convert the ... to a function that returns their evaluated values.
@@ -490,24 +495,37 @@ bindCache.reactiveExpr <- function(x, ..., cache = "app") {
cacheHint <- rlang::hash(extractCacheHint(x))
valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)
x_classes <- class(x)
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs
# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
# Hacky workaround for issue with `%>%` preventing GC:
# https://github.com/tidyverse/magrittr/issues/229
if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv)) {
rm(list = ".", envir = .GenericCallEnv)
if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv, inherits = FALSE)) {
rm(list = ".", envir = .GenericCallEnv, inherits = FALSE)
}
res <- reactive(label = label, domain = domain, {
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
)
with_no_otel_collect({
res <- reactive(label = label, domain = domain, {
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
)
})
})
class(res) <- c("reactive.cache", class(res))
local({
impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref, fn_name = "bindCache")
})
if (has_otel_collect("reactivity")) {
res <- enable_otel_reactive_expr(res)
}
res
}
@@ -534,6 +552,7 @@ bindCache.shiny.render.function <- function(x, ..., cache = "app") {
)
}
# Passes over the otelAttrs from valueFunc to renderFunc
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
renderFunc
@@ -585,7 +604,7 @@ bindCache.shiny.renderPlot <- function(x, ...,
observe({
doResizeCheck()
})
}, label = "plot-resize")
# TODO: Make sure this observer gets GC'd if output$foo is replaced.
# Currently, if you reassign output$foo, the observer persists until the
# session ends. This is generally bad programming practice and should be

View File

@@ -196,31 +196,58 @@ bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE
valueFunc <- reactive_get_value_func(x)
valueFunc <- wrapFunctionLabel(valueFunc, "eventReactiveValueFunc", ..stacktraceon = TRUE)
label <- label %||%
sprintf('bindEvent(%s, %s)', attr(x, "observable", exact = TRUE)$.label, quos_to_label(qs))
call_srcref <- get_call_srcref(-1)
if (is.null(label)) {
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = as_default_label(sprintf(
'bindEvent(%s, %s)',
attr(x, "observable", exact = TRUE)$.label,
quos_to_label(qs)
))
)
}
x_classes <- class(x)
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs
# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
initialized <- FALSE
res <- reactive(label = label, domain = domain, ..stacktraceon = FALSE, {
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
with_no_otel_collect({
res <- reactive(label = label, domain = domain, ..stacktraceon = FALSE, {
hybrid_chain(
{
eventFunc()
},
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc())
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc())
}
)
)
})
})
class(res) <- c("reactive.event", class(res))
class(res) <- c("reactive.event", x_classes)
local({
impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref, fn_name = "bindEvent")
})
if (has_otel_collect("reactivity")) {
res <- enable_otel_reactive_expr(res)
}
res
}
@@ -249,6 +276,7 @@ bindEvent.shiny.render.function <- function(x, ..., ignoreNULL = TRUE, ignoreIni
)
}
# Passes over the otelAttrs from valueFunc to renderFunc
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.event", class(valueFunc))
renderFunc
@@ -269,7 +297,17 @@ bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
# Note that because the observer will already have been logged by this point,
# this updated label won't show up in the reactlog.
x$.label <- label %||% sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
if (is.null(label)) {
call_srcref <- get_call_srcref(-1)
x$.label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = as_default_label(
sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
)
)
} else {
x$.label <- label
}
initialized <- FALSE
@@ -302,6 +340,13 @@ bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
)
class(x) <- c("Observer.event", class(x))
call_srcref <- get_call_srcref(-1)
x$.otelAttrs <- append_otel_srcref_attrs(x$.otelAttrs, call_srcref, fn_name = "bindEvent")
if (has_otel_collect("reactivity")) {
x <- enable_otel_observe(x)
}
invisible(x)
}

View File

@@ -87,13 +87,61 @@ getCallNamesForHash <- function(calls) {
})
}
# Get the preferred filename from a srcfile object.
#
# For user code, prefer the original path (as typed by user, potentially a
# symlink or relative path) over the normalized absolute path.
#
# For package files (under .libPaths()), keep the srcfile$filename because
# when a package is installed with keep.source.pkgs = TRUE, the original
# srcfilecopy filename may point to a collated build-time path rather than
# the real installed package path.
getSrcfileFilename <- function(srcfile) {
if (!is.null(srcfile$original) &&
!is.null(srcfile$original$filename) &&
!isPackageFile(srcfile$filename)) {
srcfile$original$filename
} else {
srcfile$filename
}
}
# Get the source lines and correct line number from a srcfile + srcref.
#
# sourceUTF8() wraps user code with a `#line` directive that remaps line
# numbers. This means srcref[1] (the remapped line) may not correctly index
# into the srcfile's $lines. When a #line directive is present, R extends
# the srcref to 8 elements: [7] and [8] are the original (pre-remap) first
# and last line numbers in the srcfilecopy's coordinate system.
#
# Additionally, when the #line path differs from the srcfilecopy filename
# (e.g. macOS /tmp -> /private/tmp, or Windows path normalization), R wraps
# the srcfile in a srcfilealias whose $lines is NULL. In that case, we
# retrieve lines from the original srcfilecopy via $original.
getSrcfileLines <- function(srcfile, srcref) {
lines <- srcfile$lines
line_num <- srcref[1]
if (is.null(lines) && inherits(srcfile, "srcfilealias")) {
lines <- srcfile$original$lines
}
# Use the pre-remap line number when available and different from the
# remapped line, indicating a #line directive shifted line numbering.
if (isTRUE(length(srcref) >= 7 && srcref[7] != srcref[1])) {
line_num <- srcref[7]
}
list(lines = lines, line_num = line_num)
}
getLocs <- function(calls) {
vapply(calls, function(call) {
srcref <- attr(call, "srcref", exact = TRUE)
if (!is.null(srcref)) {
srcfile <- attr(srcref, "srcfile", exact = TRUE)
if (!is.null(srcfile) && !is.null(srcfile$filename)) {
loc <- paste0(srcfile$filename, "#", srcref[[1]])
loc <- paste0(getSrcfileFilename(srcfile), "#", srcref[[1]])
return(paste0(" [", loc, "]"))
}
}
@@ -101,13 +149,36 @@ getLocs <- function(calls) {
}, character(1))
}
# Check if a file path is in an R package library
isPackageFile <- function(filepath) {
if (is.null(filepath) || filepath == "") {
return(FALSE)
}
# Normalize paths for comparison
filepath <- normalizePath(filepath, winslash = "/", mustWork = FALSE)
lib_paths <- normalizePath(.libPaths(), winslash = "/", mustWork = FALSE)
# Ensure trailing slash for proper path-boundary matching, otherwise
# e.g. "/usr/lib/R" would incorrectly match "/usr/lib/Rcpp/..."
lib_paths <- paste0(sub("/$", "", lib_paths), "/")
# Check if the file is under any library path
any(vapply(
lib_paths,
function(lib) identical(substr(filepath, 1, nchar(lib)), lib),
logical(1)
))
}
getCallCategories <- function(calls) {
vapply(calls, function(call) {
srcref <- attr(call, "srcref", exact = TRUE)
if (!is.null(srcref)) {
srcfile <- attr(srcref, "srcfile", exact = TRUE)
if (!is.null(srcfile)) {
if (!is.null(srcfile$original)) {
if (!is.null(srcfile) && !is.null(srcfile$filename)) {
# Use the absolute path for package detection (srcfile$filename)
# rather than the original path which might be relative
if (isPackageFile(srcfile$filename)) {
return("pkg")
} else {
return("user")
@@ -134,7 +205,9 @@ getCallCategories <- function(calls) {
#' @rdname stacktrace
#' @export
captureStackTraces <- function(expr) {
promises::with_promise_domain(createStackTracePromiseDomain(),
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(
createStackTracePromiseDomain(),
expr
)
}
@@ -184,7 +257,7 @@ createStackTracePromiseDomain <- function() {
# These are actually stateless, we wouldn't have to create a new one each time
# if we didn't want to. They're pretty cheap though.
d <- promises::new_promise_domain(
d <- new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
# Subscription time
@@ -278,7 +351,7 @@ withLogErrors <- function(expr,
result <- captureStackTraces(expr)
# Handle expr being an async operation
if (promises::is.promise(result)) {
if (is.promise(result)) {
result <- promises::catch(result, function(cond) {
# Don't print shiny.silent.error (i.e. validation errors)
if (cnd_inherits(cond, "shiny.silent.error")) {
@@ -443,45 +516,93 @@ printOneStackTrace <- function(stackTrace, stripResult, full, offset) {
invisible(st)
}
# Filter stack traces using fence markers to hide internal Shiny frames.
#
# `stackTraces` is a list of character vectors (call names), one per "segment".
# A single synchronous error produces one segment (the immediate call stack).
# Asynchronous errors (e.g. from promises) produce multiple segments: the deep
# stack trace segments come first, then the current segment last. Each deep
# segment may begin with frames that overlap the previous segment; a
# `..stacktracefloor..` marker delimits this redundant prefix from the active
# portion.
#
# Within the active frames, `..stacktraceon..` / `..stacktraceoff..` markers
# act as fences. Frames between a matched off/on pair (reading innermost to
# outermost) are hidden — these are the internal rendering pipeline frames
# that users don't need to see. The algorithm uses a *reverse clamped cumulative
# sum* so that an unmatched `..stacktraceoff..` (one with no corresponding
# inner `..stacktraceon..`) is a no-op, preventing it from hiding user frames.
# Fence matching works globally across segments so that a `..stacktraceoff..`
# at the end of one segment can pair with a `..stacktraceon..` at the start
# of the next.
stripStackTraces <- function(stackTraces, values = FALSE) {
score <- 1L # >=1: show, <=0: hide
lapply(seq_along(stackTraces), function(i) {
res <- stripOneStackTrace(stackTraces[[i]], i != 1, score)
score <<- res$score
toShow <- as.logical(res$trace)
if (values) {
as.character(stackTraces[[i]][toShow])
} else {
as.logical(toShow)
n_segs <- length(stackTraces)
if (n_segs == 0L) return(list())
# Replace NULL segments with empty character vectors
stackTraces <- lapply(stackTraces, function(st) st %||% character(0))
seg_lengths <- lengths(stackTraces)
total <- sum(seg_lengths)
if (total == 0L) {
return(lapply(seg_lengths, function(n) {
if (values) character(0) else logical(0)
}))
}
# Pre-compute segment boundaries (used in steps 1 and 4)
seg_ends <- cumsum(seg_lengths)
seg_starts <- c(1L, seg_ends[-n_segs] + 1L)
# Concatenate all segments into one vector for vectorized operations
all <- unlist(stackTraces)
# 1. Identify prefix elements (at/before last ..stacktracefloor.. in segs 2+)
# Prefix elements are always hidden and excluded from fence scoring.
is_active <- rep.int(TRUE, total)
if (n_segs >= 2L) {
for (i in 2:n_segs) {
if (seg_lengths[i] == 0L) next
seg_idx <- seg_starts[i]:seg_ends[i]
floor_pos <- which(all[seg_idx] == "..stacktracefloor..")
if (length(floor_pos)) {
is_active[seg_idx[seq_len(floor_pos[length(floor_pos)])]] <- FALSE
}
}
}
# 2. Compute fence scores and marker mask (vectorized across all segments)
is_on <- all == "..stacktraceon.."
is_off <- all == "..stacktraceoff.."
is_marker <- is_on | is_off | (all == "..stacktracefloor..")
scores <- integer(total)
scores[is_active & is_on] <- 1L
scores[is_active & is_off] <- -1L
# 3. Reverse clamped cumsum across all segments.
# Process from innermost (right) to outermost (left). ..stacktraceon.. (+1)
# opens a hidden region working outward, ..stacktraceoff.. (-1) closes it.
# Clamping at 0 means an unmatched ..stacktraceoff.. (one with no inner
# ..stacktraceon..) is a no-op. Prefix elements have score 0 and pass the
# running total through unchanged.
#
# Vectorized via the identity: clamped_cumsum = cumsum - pmin(0, cummin(cumsum))
rs <- rev(scores)
cs <- cumsum(rs)
depth <- rev(cs - pmin.int(0L, cummin(cs)))
# 4. Compute visibility (vectorized) and split back into segments
toShow <- is_active & depth == 0L & !is_marker
lapply(seq_len(n_segs), function(i) {
if (seg_lengths[i] == 0L) {
if (values) return(character(0)) else return(logical(0))
}
idx <- seg_starts[i]:seg_ends[i]
if (values) as.character(all[idx[toShow[idx]]]) else toShow[idx]
})
}
stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
prefix <- logical(0)
if (truncateFloor) {
indexOfFloor <- utils::tail(which(stackTrace == "..stacktracefloor.."), 1)
if (length(indexOfFloor)) {
stackTrace <- stackTrace[(indexOfFloor+1L):length(stackTrace)]
prefix <- rep_len(FALSE, indexOfFloor)
}
}
if (length(stackTrace) == 0) {
return(list(score = startingScore, character(0)))
}
score <- rep.int(0L, length(stackTrace))
score[stackTrace == "..stacktraceon.."] <- 1L
score[stackTrace == "..stacktraceoff.."] <- -1L
score <- startingScore + cumsum(score)
toShow <- score > 0 & !(stackTrace %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
list(score = utils::tail(score, 1), trace = c(prefix, toShow))
}
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
# that prunes each subtree so that only the final branch remains. The result,
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
@@ -556,8 +677,9 @@ dropTrivialTestFrames <- function(callnames) {
"testthat::test_local"
)
firstGoodCall <- min(which(!hideable))
toRemove <- firstGoodCall - 1L
# Remove everything from inception to calling the test
# It shouldn't matter how you get there, just that you're finally testing
toRemove <- max(which(hideable))
c(
rep_len(FALSE, toRemove),

View File

@@ -41,6 +41,28 @@
#' is, a function that quickly returns a promise) and allows even that very
#' session to immediately unblock and carry on with other user interactions.
#'
#' @section OpenTelemetry Integration:
#'
#' When an `ExtendedTask` is created, if OpenTelemetry tracing is enabled for
#' `"reactivity"` (see [withOtelCollect()]), the `ExtendedTask` will record
#' spans for each invocation of the task. The tracing level at `invoke()` time
#' does not affect whether spans are recorded; only the tracing level when
#' calling `ExtendedTask$new()` matters.
#'
#' The OTel span will be named based on the label created from the variable the
#' `ExtendedTask` is assigned to. If no label can be determined, the span will
#' be named `<anonymous>`. Similar to other Shiny OpenTelemetry spans, the span
#' will also include source reference attributes and session ID attributes.
#'
#' ```r
#' withOtelCollect("all", {
#' my_task <- ExtendedTask$new(function(...) { ... })
#' })
#'
#' # Span recorded for this invocation: ExtendedTask my_task
#' my_task$invoke(...)
#' ```
#'
#' @examplesIf rlang::is_interactive() && rlang::is_installed("mirai")
#' library(shiny)
#' library(bslib)
@@ -116,10 +138,37 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
#' read reactive inputs and pass them as arguments.
initialize = function(func) {
private$func <- func
private$rv_status <- reactiveVal("initial")
private$rv_value <- reactiveVal(NULL)
private$rv_error <- reactiveVal(NULL)
# Do not show these private reactive values in otel spans
with_no_otel_collect({
private$rv_status <- reactiveVal("initial", label = "ExtendedTask$private$status")
private$rv_value <- reactiveVal(NULL, label = "ExtendedTask$private$value")
private$rv_error <- reactiveVal(NULL, label = "ExtendedTask$private$error")
})
private$invocation_queue <- fastmap::fastqueue()
domain <- getDefaultReactiveDomain()
# Set a label for the reactive values for easier debugging
# Go up an extra sys.call() to get the user's call to ExtendedTask$new()
# The first sys.call() is to `initialize(...)`
call_srcref <- get_call_srcref(-1)
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = "<anonymous>"
)
private$otel_span_label <- otel_span_label_extended_task(label, domain = domain)
private$otel_log_label_add_to_queue <- otel_log_label_extended_task_add_to_queue(label, domain = domain)
private$otel_attrs <- c(
otel_srcref_attributes(call_srcref, "ExtendedTask"),
otel_session_id_attrs(domain)
) %||% list()
# Capture this value at init-time, not run-time
# This way, the span is only created if otel was enabled at time of creation... just like other spans
private$is_recording_otel <- has_otel_collect("reactivity")
},
#' @description
#' Starts executing the long-running operation. If this `ExtendedTask` is
@@ -139,8 +188,27 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
isolate(private$rv_status()) == "running" ||
private$invocation_queue$size() > 0
) {
otel_log(
private$otel_log_add_to_queue_label,
severity = "debug",
attributes = c(
private$otel_attrs,
list(
queue_size = private$invocation_queue$size() + 1L
)
)
)
private$invocation_queue$add(list(args = args, call = call))
} else {
if (private$is_recording_otel) {
private$otel_span <- start_otel_span(
private$otel_span_label,
attributes = private$otel_attrs
)
otel::local_active_span(private$otel_span)
}
private$do_invoke(args, call = call)
}
invisible(NULL)
@@ -188,7 +256,7 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
#' invalidation will be ignored.
result = function() {
switch (private$rv_status(),
running = req(FALSE, cancelOutput="progress"),
running = req(FALSE, cancelOutput = "progress"),
success = if (private$rv_value()$visible) {
private$rv_value()$value
} else {
@@ -208,21 +276,36 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
rv_error = NULL,
invocation_queue = NULL,
otel_span_label = NULL,
otel_log_label_add_to_queue = NULL,
otel_attrs = list(),
is_recording_otel = FALSE,
otel_span = NULL,
do_invoke = function(args, call = NULL) {
private$rv_status("running")
private$rv_value(NULL)
private$rv_error(NULL)
p <- promises::promise_resolve(
p <- promise_resolve(
maskReactiveContext(do.call(private$func, args))
)
p <- promises::then(
p,
onFulfilled = function(value, .visible) {
if (is_otel_span(private$otel_span)) {
private$otel_span$end(status_code = "ok")
private$otel_span <- NULL
}
private$on_success(list(value = value, visible = .visible))
},
onRejected = function(error) {
if (is_otel_span(private$otel_span)) {
private$otel_span$end(status_code = "error")
private$otel_span <- NULL
}
private$on_error(error, call = call)
}
)

View File

@@ -25,3 +25,7 @@ on_load_exprs <- list()
on_load <- function(expr) {
on_load_exprs[[length(on_load_exprs) + 1]] <<- substitute(expr)
}
on_load({
IS_SHINY_LOCAL_PKG <- exists(".__DEVTOOLS__")
})

View File

@@ -100,7 +100,7 @@ plotPNG <- function(func, filename=tempfile(fileext='.png'),
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
force(which)
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {

View File

@@ -59,11 +59,11 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL,
icon <- validateIcon(icon)
if (!is.null(icon)) {
icon <- span(icon, class = "action-icon")
icon <- span(icon, class = "action-icon", .noWS = c("outside", "inside"))
}
if (!is.null(label)) {
label <- span(label, class = "action-label")
label <- span(label, class = "action-label", .noWS = c("outside", "inside"))
}
tags$button(
@@ -74,6 +74,7 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL,
`data-val` = value,
disabled = if (isTRUE(disabled)) NA else NULL,
icon, label,
.noWS = "inside",
...
)
}
@@ -86,11 +87,11 @@ actionLink <- function(inputId, label, icon = NULL, ...) {
icon <- validateIcon(icon)
if (!is.null(icon)) {
icon <- span(icon, class = "action-icon")
icon <- span(icon, class = "action-icon", .noWS = c("outside", "inside"))
}
if (!is.null(label)) {
label <- span(label, class = "action-label")
label <- span(label, class = "action-label", .noWS = c("outside", "inside"))
}
tags$a(
@@ -99,6 +100,7 @@ actionLink <- function(inputId, label, icon = NULL, ...) {
class = "action-button action-link",
`data-val` = value,
icon, label,
.noWS = "inside",
...
)
}

View File

@@ -436,34 +436,36 @@ MockShinySession <- R6Class(
if (!is.function(func))
stop(paste("Unexpected", class(func), "output for", name))
obs <- observe({
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
prom <- NULL
tryCatch({
v <- private$withCurrentOutput(name, func(self, name))
if (!promises::is.promise(v)){
# Make our sync value into a promise
prom <- promises::promise(function(resolve, reject){ resolve(v) })
} else {
prom <- v
}
}, error=function(e){
# Error running value()
prom <<- promises::promise(function(resolve, reject){ reject(e) })
})
private$outs[[name]]$promise <- hybrid_chain(
prom,
function(v){
list(val = v, err = NULL)
}, catch=function(e){
if (
!inherits(e, c("shiny.custom.error", "shiny.output.cancel", "shiny.output.progress", "shiny.silent.error"))
) {
self$unhandledError(e, close = FALSE)
with_no_otel_collect({
obs <- observe({
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
prom <- NULL
tryCatch({
v <- private$withCurrentOutput(name, func(self, name))
if (!is.promise(v)){
# Make our sync value into a promise
prom <- promise_resolve(v)
} else {
prom <- v
}
list(val = NULL, err = e)
}, error=function(e){
# Error running value()
prom <<- promise_reject(e)
})
private$outs[[name]]$promise <- hybrid_chain(
prom,
function(v){
list(val = v, err = NULL)
}, catch=function(e){
if (
!inherits(e, c("shiny.custom.error", "shiny.output.cancel", "shiny.output.progress", "shiny.silent.error"))
) {
self$unhandledError(e, close = FALSE)
}
list(val = NULL, err = e)
})
})
})
private$outs[[name]] <- list(obs = obs, func = func, promise = NULL)
},
@@ -716,7 +718,7 @@ MockShinySession <- R6Class(
stop("Nested calls to withCurrentOutput() are not allowed.")
}
promises::with_promise_domain(
with_promise_domain(
createVarPromiseDomain(private, "currentOutputName", name),
expr
)

67
R/otel-attr-srcref.R Normal file
View File

@@ -0,0 +1,67 @@
# Very similar to srcrefFromShinyCall(),
# however, this works when the function does not have a srcref attr set
otel_srcref_attributes <- function(srcref, fn_name = NULL) {
if (is.function(srcref)) {
srcref <- getSrcRefs(srcref)[[1]][[1]]
}
if (is.null(srcref)) {
return(NULL)
}
stopifnot(inherits(srcref, "srcref"))
# Semantic conventions for code: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/
#
# Inspiration from https://github.com/r-lib/testthat/pull/2087/files#diff-92de3306849d93d6f7e76c5aaa1b0c037e2d716f72848f8a1c70536e0c8a1564R123-R124
srcfile <- attr(srcref, "srcfile")
# Prefer the original filename (as user typed it) over the normalized path
filename <- getSrcfileFilename(srcfile)
dropNulls(list(
"code.function.name" = fn_name,
# Location attrs
"code.file.path" = filename,
"code.line.number" = srcref[1],
"code.column.number" = srcref[2],
# Remove these deprecated location names once Logfire supports the preferred names
# https://github.com/pydantic/logfire/issues/1559
"code.filepath" = filename,
"code.lineno" = srcref[1],
"code.column" = srcref[2]
))
}
#' Get the srcref for the call at the specified stack level
#'
#' If you need to go farther back in the `sys.call()` stack, supply a larger
#' negative number to `which_offset`. The default of 0 gets the immediate
#' caller. `-1` would get the caller's caller, and so on.
#' @param which_offset The stack level to get the call from. Defaults to -1 (the
#' immediate caller).
#' @return An srcref object, or NULL if none is found.
#' @noRd
get_call_srcref <- function(which_offset = 0) {
# Go back one call to account for this function itself
call <- sys.call(which_offset - 1)
srcref <- attr(call, "srcref", exact = TRUE)
srcref
}
append_otel_srcref_attrs <- function(attrs, call_srcref, fn_name) {
if (is.null(call_srcref)) {
return(attrs)
}
srcref_attrs <- otel_srcref_attributes(call_srcref, fn_name)
if (is.null(srcref_attrs)) {
return(attrs)
}
attrs[names(srcref_attrs)] <- srcref_attrs
attrs
}

55
R/otel-collect.R Normal file
View File

@@ -0,0 +1,55 @@
otel_collect_choices <- c(
"none",
"session",
"reactive_update",
"reactivity",
"all"
)
# Check if the collect level is sufficient
otel_collect_is_enabled <- function(
impl_level,
# Listen to option and fall back to the env var
opt_collect_level = getOption("shiny.otel.collect", Sys.getenv("SHINY_OTEL_COLLECT", "all"))
) {
opt_collect_level <- as_otel_collect(opt_collect_level)
which(opt_collect_level == otel_collect_choices) >=
which(impl_level == otel_collect_choices)
}
# Check if tracing is enabled and if the collect level is sufficient
has_otel_collect <- function(collect) {
# Only check pkg author input iff loaded with pkgload
if (IS_SHINY_LOCAL_PKG) {
stopifnot(length(collect) == 1, any(collect == otel_collect_choices))
}
otel_is_tracing_enabled() && otel_collect_is_enabled(collect)
}
# Run expr with otel collection disabled
with_no_otel_collect <- function(expr) {
withOtelCollect("none", expr)
}
## -- Helpers -----------------------------------------------------
# shiny.otel.collect can be:
# "none"; To do nothing / fully opt-out
# "session" for session/start events
# "reactive_update" (includes "session" features) and reactive_update spans
# "reactivity" (includes "reactive_update" features) and spans for all reactive things
# "all" - Anything that Shiny can do. (Currently equivalent to the "reactivity" level)
as_otel_collect <- function(collect = "all") {
if (!is.character(collect)) {
stop("`collect` must be a character vector.")
}
# Match to collect enum
collect <- match.arg(collect, otel_collect_choices, several.ok = FALSE)
return(collect)
}

194
R/otel-enable.R Normal file
View File

@@ -0,0 +1,194 @@
# # Approach
# Use flags on the reactive object to indicate whether to record OpenTelemetry spans.
#
# Cadence:
# * `$.isRecordingOtel` - Whether to record OpenTelemetry spans for this reactive object
# * `$.otelLabel` - The label to use for the OpenTelemetry span
# * `$.otelAttrs` - Additional attributes to add to the OpenTelemetry span
#' Add OpenTelemetry for reactivity to an object
#'
#' @description
#'
#' `enable_otel_*()` methods add OpenTelemetry flags for [reactive()] expressions
#' and `render*` functions (like [renderText()], [renderTable()], ...).
#'
#' Wrapper to creating an active reactive OpenTelemetry span that closes when
#' the reactive expression is done computing. Typically this is when the
#' reactive expression finishes (synchronous) or when the returned promise is
#' done computing (asynchronous).
#' @section Async with OpenTelemetry:
#'
#' With a reactive expression, the key and/or value expression can be
#' _asynchronous_. In other words, they can be promises --- not regular R
#' promises, but rather objects provided by the
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
#' are similar to promises in JavaScript. (See [promises::promise()] for more
#' information.) You can also use [mirai::mirai()] or [future::future()]
#' objects to run code in a separate process or even on a remote machine.
#'
#' When reactive expressions are being calculated in parallel (by having
#' another reactive promise compute in the main process), the currently active
#' OpenTelemetry span will be dynamically swapped out according to the
#' currently active reactive expression. This means that as long as a promise
#' was `then()`ed or `catch()`ed with an active OpenTelemetry span, the span
#' will be correctly propagated to the next step (and subsequently other
#' steps) in the promise chain.
#'
#' While the common case is for a reactive expression to be created
#' synchronously, troubles arise when the reactive expression is created
#' asynchronously. The span **must** be created before the reactive expression
#' is executed, it **must** be active for the duration of the expression, and
#' it **must** not be closed until the reactive expression is done executing.
#' This is not easily achieved with a single function call, so we provide a
#' way to create a reactive expression that is bound to an OpenTelemetry
#' span.
#'
#' @section Span management and performance:
#'
#' Dev note - Barret 2025-10:
#' Typically, an OpenTelemetry span (`otel_span`) will inherit from the parent
#' span. This works well and we can think of the hierarchy as a tree. With
#' `options("shiny.otel.collect" = <value>)`, we are able to control with a sliding
#' dial how much of the tree we are interested in: "none", "session",
#' "reactive_update", "reactivity", and finally "all".
#'
#' Leveraging this hierarchy, we can avoid creating spans that are not needed.
#' The act of making a noop span takes on the order of 10microsec. Handling of
#' the opspan is also in the 10s of microsecond range. We should avoid this when
#' we **know** that we're not interested in the span. Therefore, manually
#' handling spans should be considered for Shiny.
#'
#' * Q:
#' * But what about app author who want the current span? Is there any
#' guarantee that the current span is expected `reactive()` span?
#' * A:
#' * No. The current span is whatever the current span is. If the app author
#' wants a specific span, they should create it themselves.
#' * Proof:
#' ```r
#' noop <- otel::get_active_span()
#' noop$get_context()$get_span_id()
#' #> [1] "0000000000000000"
#' ignore <- otelsdk::with_otel_record({
#' a <- otel::start_local_active_span("a")
#' a$get_context()$get_span_id() |> str()
#' otel::with_active_span(noop, {
#' otel::get_active_span()$get_context()$get_span_id() |> str()
#' })
#' })
#' #> chr "2645e95715841e75"
#' #> chr "2645e95715841e75"
#' # ## It is reasonable to expect the second id to be `0000000000000000`, but it's not.
#' ```
#' Therefore, the app author has no guarantee that the current span is the
#' span they're expecting. If the app author wants a specific span, they should
#' create it themselves and let natural inheritance take over.
#'
#' Given this, I will imagine that app authors will set
#' `options("shiny.otel.collect" = "reactive_update")` as their default behavior.
#' Enough to know things are happening, but not overwhelming from **everything**
#' that is reactive.
#'
#' To _light up_ a specific area, users can call `withr::with_options(list("shiny.otel.collect" = "all"), { ... })`.
#'
#' @param x The object to add caching to.
#' @param ... Future parameter expansion.
#' @noRd
NULL
enable_otel_reactive_val <- function(x) {
impl <- attr(x, ".impl", exact = TRUE)
# Set flag for otel logging when setting the value
impl$.isRecordingOtel <- TRUE
class(x) <- c("reactiveVal.otel", class(x))
x
}
enable_otel_reactive_values <- function(x) {
impl <- .subset2(x, "impl")
# Set flag for otel logging when setting values
impl$.isRecordingOtel <- TRUE
class(x) <- c("reactivevalues.otel", class(x))
x
}
enable_otel_reactive_expr <- function(x) {
domain <- reactive_get_domain(x)
impl <- attr(x, "observable", exact = TRUE)
impl$.isRecordingOtel <- TRUE
# Covers both reactive and reactive.event
impl$.otelLabel <- otel_span_label_reactive(x, domain = impl$.domain)
class(x) <- c("reactiveExpr.otel", class(x))
x
}
enable_otel_observe <- function(x) {
x$.isRecordingOtel <- TRUE
x$.otelLabel <- otel_span_label_observer(x, domain = x$.domain)
class(x) <- c("Observer.otel", class(x))
invisible(x)
}
enable_otel_shiny_render_function <- function(x) {
valueFunc <- force(x)
otel_span_label <- NULL
otel_span_attrs <- NULL
renderFunc <- function(...) {
# Dynamically determine the span label given the current reactive domain
if (is.null(otel_span_label)) {
domain <- getDefaultReactiveDomain()
otel_span_label <<-
otel_span_label_render_function(x, domain = domain)
otel_span_attrs <<- c(
attr(x, "otelAttrs"),
otel_session_id_attrs(domain)
)
}
with_otel_span(
otel_span_label,
{
hybrid_then(
valueFunc(...),
on_failure = set_otel_exception_status_and_throw,
# Must save the error object
tee = FALSE
)
},
attributes = otel_span_attrs
)
}
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.otel", class(valueFunc))
renderFunc
}
# ## If we ever expose a S3 function, I'd like to add this method.
# bindOtel.function <- function(x, ...) {
# cli::cli_abort(paste0(
# "Don't know how to add OpenTelemetry recording to a plain function. ",
# "If this is a {.code render*()} function for Shiny, it may need to be updated. ",
# "Please see {.help shiny::bindOtel} for more information."
# ))
# }

56
R/otel-error.R Normal file
View File

@@ -0,0 +1,56 @@
has_seen_otel_exception <- function(cnd) {
!is.null(cnd$.shiny_otel_exception)
}
mark_otel_exception_as_seen <- function(cnd) {
cnd$.shiny_otel_exception <- TRUE
cnd
}
set_otel_exception_status_and_throw <- function(cnd) {
cnd <- set_otel_exception_status(cnd)
# Rethrow the (possibly updated) error
signalCondition(cnd)
}
set_otel_exception_status <- function(cnd) {
if (inherits(cnd, "shiny.custom.error")) {
# No-op
} else if (inherits(cnd, "shiny.output.cancel")) {
# No-op
} else if (inherits(cnd, "shiny.output.progress")) {
# No-op
} else if (cnd_inherits(cnd, "shiny.silent.error")) {
# No-op
} else {
# Only when an unknown error occurs do we set the span status to error
span <- otel::get_active_span()
# Only record the exception once at the original point of failure,
# not every reactive expression that it passes through
if (!has_seen_otel_exception(cnd)) {
span$record_exception(
# Record a sanitized error if sanitization is enabled
get_otel_error_obj(cnd)
)
cnd <- mark_otel_exception_as_seen(cnd)
}
# Record the error status on the span for any context touching this error
span$set_status("error")
}
cnd
}
get_otel_error_obj <- function(e) {
# Do not expose errors to otel if sanitization is enabled
if (getOption("shiny.otel.sanitize.errors", TRUE)) {
sanitized_error()
} else {
e
}
}

198
R/otel-label.R Normal file
View File

@@ -0,0 +1,198 @@
# observe mymod:<anonymous>
# observe <anonymous>
# observe mylabel
# -- Reactives --------------------------------------------------------------
#' OpenTelemetry Label Generation Functions
#'
#' Functions for generating formatted labels for OpenTelemetry tracing spans
#' in Shiny applications. These functions handle module namespacing and
#' cache/event modifiers for different Shiny reactive constructs.
#'
#' @param x The object to generate a label for (reactive, observer, etc.)
#' @param label Character string label for reactive values
#' @param key Character string key for reactiveValues operations
#' @param ... Additional arguments (unused)
#' @param domain Shiny domain object containing namespace information
#'
#' @return Character string formatted for OpenTelemetry span labels
#' @name otel_label
#' @noRd
NULL
otel_span_label_reactive <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"reactive",
cache_class = "reactive.cache",
event_class = "reactive.event"
)
label <- attr(x, "observable", exact = TRUE)[[".label"]]
otel_span_label <- otel_label_upgrade(label, domain = domain)
sprintf("%s %s", fn_name, otel_span_label)
}
otel_span_label_render_function <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"output",
cache_class = "shiny.render.function.cache",
event_class = "shiny.render.function.event"
)
label <- getCurrentOutputInfo(session = domain)$name %||% "<unknown>"
otel_span_label <- otel_label_upgrade(label, domain = domain)
sprintf("%s %s", fn_name, otel_span_label)
}
otel_span_label_observer <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"observe",
cache_class = NULL, # Do not match a cache class here
event_class = "Observer.event"
)
otel_span_label <- otel_label_upgrade(x$.label, domain = domain)
sprintf("%s %s", fn_name, otel_span_label)
}
# -- Set reactive value(s) ----------------------------------------------------
otel_log_label_set_reactive_val <- function(label, ..., domain) {
sprintf(
"Set reactiveVal %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_log_label_set_reactive_values <- function(label, key, ..., domain) {
sprintf(
"Set reactiveValues %s$%s",
otel_label_upgrade(label, domain = domain),
key
)
}
# -- ExtendedTask -------------------------------------------------------------
otel_span_label_extended_task <- function(label, suffix = NULL, ..., domain) {
sprintf(
"ExtendedTask %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_log_label_extended_task_add_to_queue <- function(label, ..., domain) {
sprintf(
"ExtendedTask %s add to queue",
otel_label_upgrade(label, domain = domain)
)
}
# -- Debounce / Throttle -------------------------------------------------------
otel_label_debounce <- function(label, ..., domain) {
sprintf(
"debounce %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_label_throttle <- function(label, ..., domain) {
sprintf(
"throttle %s",
otel_label_upgrade(label, domain = domain)
)
}
# ---- Reactive Poll / File Reader -----------------------------------------------
otel_label_reactive_poll <- function(label, ..., domain) {
sprintf(
"reactivePoll %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_label_reactive_file_reader <- function(label, ..., domain) {
sprintf(
"reactiveFileReader %s",
otel_label_upgrade(label, domain = domain)
)
}
# -- Helpers --------------------------------------------------------------
#' Modify function name based on object class modifiers
#'
#' @param x Object to check class of
#' @param fn_name Base function name
#' @param cache_class Optional class name that indicates cache operation
#' @param event_class Optional class name that indicates event operation
#'
#' @return Modified function name with "cache" or "event" suffix if applicable
#' @noRd
otel_label_with_modifiers <- function(
x,
fn_name,
cache_class = NULL,
event_class = NULL
) {
for (x_class in rev(class(x))) {
if (!is.null(cache_class) && x_class == cache_class) {
fn_name <- sprintf("%s cache", fn_name)
} else if (!is.null(event_class) && x_class == event_class) {
fn_name <- sprintf("%s event", fn_name)
}
}
fn_name
}
#' Upgrade and format OpenTelemetry labels with module namespacing
#'
#' Processes labels for OpenTelemetry tracing, replacing default verbose labels
#' with cleaner alternatives and prepending module namespaces when available.
#'
#' @param label Character string label to upgrade
#' @param ... Additional arguments (unused)
#' @param domain Shiny domain object containing namespace information
#'
#' @return Modified label string with module prefix if applicable
#' @noRd
#'
#' @details
#' Module prefix examples:
#' - "" -> ""
#' - "my-nested-mod-" -> "my-nested-mod"
otel_label_upgrade <- function(label, ..., domain) {
# By default, `observe()` sets the label to `observe(CODE)`
# This label is too big and inconsistent.
# Replace it with `<anonymous>`
# (Similar with `eventReactive()` and `observeEvent()`)
if (is_default_label(label) && grepl("(", label, fixed = TRUE)) {
label <- "<anonymous>"
# label <- sprintf("<anonymous> - %s", label)
}
if (is.null(domain)) {
return(label)
}
namespace <- domain$ns("")
if (!nzchar(namespace)) {
return(label)
}
# Remove trailing module separator
mod_ns <- sub(sprintf("%s$", ns.sep), "", namespace)
# Prepend the module name to the label
# Ex: `"mymod:x"`
sprintf("%s:%s", mod_ns, label)
}

114
R/otel-reactive-update.R Normal file
View File

@@ -0,0 +1,114 @@
# * `session$userData[["_otel_span_reactive_update"]]` - The active reactive update span (or `NULL`)
#' Start a `reactive_update` OpenTelemetry span and store it
#'
#' Used when a reactive expression is updated
#' Will only start the span iff the otel tracing is enabled
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @return Invisibly returns.
#' @seealso `otel_span_reactive_update_teardown()`
#' @noRd
otel_span_reactive_update_init <- function(..., domain) {
if (!has_otel_collect("reactive_update")) return()
# Ensure cleanup is registered only once per session
if (is.null(domain$userData[["_otel_has_reactive_cleanup"]])) {
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
# Clean up any dangling reactive spans on an unplanned exit
domain$onSessionEnded(function() {
otel_span_reactive_update_teardown(domain = domain)
})
}
# Safety check
if (is_otel_span(domain$userData[["_otel_span_reactive_update"]])) {
stop("Reactive update span already exists")
}
domain$userData[["_otel_span_reactive_update"]] <-
start_otel_span(
"reactive_update",
...,
attributes = otel_session_id_attrs(domain)
)
invisible()
}
#' End a `reactive_update` OpenTelemetry span and remove it from the session
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @return Invisibly returns.
#' @seealso `otel_span_reactive_update_init()`
#' @noRd
otel_span_reactive_update_teardown <- function(..., domain) {
ospan <- domain$userData[["_otel_span_reactive_update"]]
if (is_otel_span(ospan)) {
otel::end_span(ospan)
domain$userData[["_otel_span_reactive_update"]] <- NULL
}
invisible()
}
#' Run expr within a `reactive_update` OpenTelemetry span
#'
#' Used to wrap the execution of a reactive expression. Will only
#' require/activate the span iff the otel tracing is enabled
#' @param expr The expression to executed within the span
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @noRd
with_otel_span_reactive_update <- function(expr, ..., domain) {
ospan <- domain$userData[["_otel_span_reactive_update"]]
if (!is_otel_span(ospan)) {
return(force(expr))
}
# Given the reactive update span is started before and ended when exec count
# is 0, we only need to wrap the expr in the span context
otel::with_active_span(ospan, {force(expr)})
}
#' Run expr within `reactive_update` otel span if not already active
#'
#' If the reactive update otel span is not already active, run the expression
#' within the reactive update otel span context. This ensures that nested calls
#' to reactive expressions do not attempt to re-enter the same span.
#'
#' This method is used within Context `run()` and running an Output's observer
#' implementation
#' @param expr The expression to executed within the span
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @noRd
maybe_with_otel_span_reactive_update <- function(expr, ..., domain) {
if (is.null(domain$userData[["_otel_reactive_update_is_active"]])) {
domain$userData[["_otel_reactive_update_is_active"]] <- TRUE
# When the expression is done promising, clear the active flag
hybrid_then(
{
with_otel_span_reactive_update(domain = domain, expr)
},
on_success = function(value) {
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
},
on_failure = function(e) {
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
},
# Return the value before the callbacks
tee = TRUE
)
} else {
expr
}
}

96
R/otel-session.R Normal file
View File

@@ -0,0 +1,96 @@
# Semantic conventions for session: https://opentelemetry.io/docs/specs/semconv/general/session/
#' Create and use session span and events
#'
#' If otel is disabled, the session span and events will not be created,
#' however the expression will still be evaluated.
#'
#' Span: `session_start`, `session_end`
#' @param expr Expression to evaluate within the session span
#' @param ... Ignored
#' @param domain The reactive domain
#' @noRd
otel_span_session_start <- function(expr, ..., domain) {
if (!has_otel_collect("session")) {
return(force(expr))
}
# Wrap the server initialization
with_otel_span(
"session_start",
expr,
attributes = otel::as_attributes(c(
otel_session_id_attrs(domain),
otel_session_attrs(domain)
))
)
}
otel_span_session_end <- function(expr, ..., domain) {
if (!has_otel_collect("session")) {
return(force(expr))
}
id_attrs <- otel_session_id_attrs(domain)
with_otel_span(
"session_end",
expr,
attributes = id_attrs
)
}
# -- Helpers -------------------------------
# Occurs when the websocket connection is established
otel_session_attrs <- function(domain) {
# TODO: Future: Posit Connect integration
# > we are still trying to identify all of the information we want to track/expose
#
# * `POSIT_PRODUCT` (Fallback to RSTUDIO_PRODUCT) for host environment
# * `CONNECT_SERVER` envvar to get the `session.address`.
# * `CONNECT_CONTENT_GUID` for the consistent app distinguisher
# * Maybe `CONNECT_CONTENT_JOB_KEY`?
# * Maybe `user.id` to be their user name: https://opentelemetry.io/docs/specs/semconv/registry/attributes/user/
attrs <- list(
server.path =
sub(
"/websocket/$", "/",
domain[["request"]][["PATH_INFO"]] %||% ""
),
server.address = domain[["request"]][["HTTP_HOST"]] %||% "",
server.origin = domain[["request"]][["HTTP_ORIGIN"]] %||% "",
## Currently, Shiny does not expose QUERY_STRING when connecting the websocket
# so we do not provide it here.
# QUERY_STRING = domain[["request"]][["QUERY_STRING"]] %||% "",
server.port = domain[["request"]][["SERVER_PORT"]] %||% NA_integer_
)
# Safely convert SERVER_PORT to integer
# If conversion fails, leave as-is (string or empty)
# This avoids warnings/errors if SERVER_PORT is not a valid integer
server_port <- suppressWarnings(as.integer(attrs$server.port))
if (!is.na(server_port)) {
attrs$server.port <- server_port
}
attrs
}
otel_session_id_attrs <- function(domain) {
token <- domain$token
if (is.null(token)) {
return(list())
}
list(
# Convention for client-side with session.start and session.end events
# https://opentelemetry.io/docs/specs/semconv/general/session/
#
# Since we are the server, we'll add them as an attribute to _every_ span
# within the session as we don't know exactly when they will be called.
# Given it's only a single attribute, the cost should be minimal, but it ties every reactive calculation together.
session.id = token
)
}

127
R/otel-shiny.R Normal file
View File

@@ -0,0 +1,127 @@
# Used by otel to identify the tracer and logger for this package
# https://github.com/r-lib/otel/blob/afc31bc1f4bd177870d44b051ada1d9e4e685346/R/tracer-name.R#L33-L49
# DO NOT CHANGE THIS VALUE without understanding the implications for existing telemetry data!
otel_tracer_name <- "co.posit.r-package.shiny"
init_otel <- function() {
.globals$otel_tracer <- otel::get_tracer()
.globals$otel_is_tracing_enabled <- otel::is_tracing_enabled(.globals$otel_tracer)
.globals$otel_logger <- otel::get_logger()
# .globals$otel_is_logging_enabled <- otel::is_logging_enabled()
}
on_load({init_otel()})
#' Run expr within a Shiny OpenTelemetry recording context
#'
#' Reset the OpenTelemetry tracer and logger for Shiny.
#' Used for testing purposes only.
#' @param expr Expression to evaluate within the recording context
#' @return The result of evaluating `otelsdk::with_otel_record(expr)` with freshly enabled Shiny otel tracer and logger
#' @noRd
with_shiny_otel_record <- function(expr) {
# Only use within internal testthat tests
stopifnot(testthat__is_testing())
withr::defer({ init_otel() })
otelsdk::with_otel_record({
init_otel()
force(expr)
})
}
#' Check if OpenTelemetry tracing is enabled
#'
#' @param tracer The OpenTelemetry tracer to check (default: Shiny otel tracer)
#' @return `TRUE` if tracing is enabled, `FALSE` otherwise
#' @noRd
otel_is_tracing_enabled <- function() {
.globals[["otel_is_tracing_enabled"]]
}
#' Shiny OpenTelemetry logger
#'
#' Used for logging OpenTelemetry events via `otel_log()`
#' @return An OpenTelemetry logger
#' @noRd
shiny_otel_logger <- function() {
.globals[["otel_logger"]]
}
#' Shiny OpenTelemetry tracer
#'
#' Used for creating OpenTelemetry spans via `with_otel_span()` and
#' `start_otel_span()`
#'
#' Inspired by httr2:::get_tracer().
#' @return An OpenTelemetry tracer
#' @noRd
shiny_otel_tracer <- function() {
.globals[["otel_tracer"]]
}
#' Create and use a Shiny OpenTelemetry span
#'
#' If otel is disabled, the span will not be created,
#' however the expression will still be evaluated.
#' @param name Span name
#' @param expr Expression to evaluate within the span
#' @param ... Ignored
#' @param attributes Optional span attributes
#' @return The result of evaluating `expr`
#' @noRd
with_otel_span <- function(name, expr, ..., attributes = NULL) {
promises::with_otel_span(name, expr, ..., attributes = attributes, tracer = shiny_otel_tracer())
}
#' Start a Shiny OpenTelemetry span
#'
#' @param name Span name
#' @param ... Additional arguments passed to `otel::start_span()`
#' @return An OpenTelemetry span
#' @noRd
start_otel_span <- function(name, ...) {
otel::start_span(name, ..., tracer = shiny_otel_tracer())
}
# # TODO: Set attributes on the current active span
# # 5. Set attributes on the current active span
# set_otel_span_attrs(status = 200L)
# -- Helpers --------------------------------------------------------------
is_otel_span <- function(x) {
inherits(x, "otel_span")
}
testthat__is_testing <- function() {
# testthat::is_testing()
identical(Sys.getenv("TESTTHAT"), "true")
}
#' Log a message using the Shiny OpenTelemetry logger
#'
#' @param msg The log message
#' @param ... Additional attributes to add to the log record
#' @param severity The log severity level (default: "info")
#' @param logger The OpenTelemetry logger to use (default: Shiny otel logger)
#' @return Invisibly returns.
#' @noRd
otel_log <- function(
msg,
...,
severity = "info",
logger = shiny_otel_logger()
) {
otel::log(msg, ..., severity = severity, logger = logger)
}

125
R/otel-with.R Normal file
View File

@@ -0,0 +1,125 @@
#' Temporarily set OpenTelemetry (OTel) collection level
#'
#' @description
#' Control Shiny's OTel collection level for particular reactive expression(s).
#'
#' `withOtelCollect()` sets the OpenTelemetry collection level for
#' the duration of evaluating `expr`. `localOtelCollect()` sets the collection
#' level for the remainder of the current function scope.
#'
#' @details
#' Note that `"session"` and `"reactive_update"` levels are not permitted as
#' these are runtime-specific levels that should only be set permanently via
#' `options(shiny.otel.collect = ...)` or the `SHINY_OTEL_COLLECT` environment
#' variable, not temporarily during reactive expression creation.
#'
#' @section Best practice:
#'
#' Best practice is to set the collection level for code that *creates* reactive
#' expressions, not code that *runs* them. For instance:
#'
#' ```r
#' # Disable telemetry for a reactive expression
#' withOtelCollect("none", {
#' my_reactive <- reactive({ ... })
#' })
#'
#' # Disable telemetry for a render function
#' withOtelCollect("none", {
#' output$my_plot <- renderPlot({ ... })
#' })
#'
#' #' # Disable telemetry for an observer
#' withOtelCollect("none", {
#' observe({ ... }))
#' })
#'
#' # Disable telemetry for an entire module
#' withOtelCollect("none", {
#' my_result <- my_module("my_id")
#' })
#' # Use `my_result` as normal here
#' ```
#'
#' NOTE: It's not recommended to pipe existing reactive objects into
#' `withOtelCollect()` since they won't inherit their intended OTel settings,
#' leading to confusion.
#'
#' @param collect Character string specifying the OpenTelemetry collection level.
#' Must be one of the following:
#'
#' * `"none"` - No telemetry data collected
#' * `"reactivity"` - Collect reactive execution spans (includes session and
#' reactive update events)
#' * `"all"` - All available telemetry (currently equivalent to `"reactivity"`)
#' @param expr Expression to evaluate with the specified collection level
#' (for `withOtelCollect()`).
#' @param envir Environment where the collection level should be set
#' (for `localOtelCollect()`). Defaults to the parent frame.
#'
#' @return
#' * `withOtelCollect()` returns the value of `expr`.
#' * `localOtelCollect()` is called for its side effect and returns the previous
#' `collect` value invisibly.
#'
#' @seealso See the `shiny.otel.collect` option within [`shinyOptions`]. Setting
#' this value will globally control OpenTelemetry collection levels.
#'
#' @examples
#' \dontrun{
#' # Temporarily disable telemetry collection
#' withOtelCollect("none", {
#' # Code here won't generate telemetry
#' reactive({ input$x + 1 })
#' })
#'
#' # Collect reactivity telemetry but not other events
#' withOtelCollect("reactivity", {
#' # Reactive execution will be traced
#' observe({ print(input$x) })
#' })
#'
#' # Use local variant in a function
#' my_function <- function() {
#' localOtelCollect("none")
#' # Rest of function executes without telemetry
#' reactive({ input$y * 2 })
#' }
#' }
#'
#' @rdname withOtelCollect
#' @export
withOtelCollect <- function(collect, expr) {
collect <- as_otel_collect_with(collect)
withr::with_options(
list(shiny.otel.collect = collect),
expr
)
}
#' @rdname withOtelCollect
#' @export
localOtelCollect <- function(collect, envir = parent.frame()) {
collect <- as_otel_collect_with(collect)
old <- withr::local_options(
list(shiny.otel.collect = collect),
.local_envir = envir
)
invisible(old)
}
# Helper function to validate collect levels for with/local functions
# Only allows "none", "reactivity", and "all" - not "session" or "reactive_update"
as_otel_collect_with <- function(collect) {
if (!is.character(collect)) {
stop("`collect` must be a character vector.")
}
allowed_levels <- c("none", "reactivity", "all")
collect <- match.arg(collect, allowed_levels, several.ok = FALSE)
return(collect)
}

View File

@@ -16,6 +16,60 @@ processId <- local({
}
})
ctx_otel_info_obj <- function(
isRecordingOtel = FALSE,
otelLabel = "<unknown>",
otelAttrs = list()
) {
structure(
list(
isRecordingOtel = isRecordingOtel,
otelLabel = otelLabel,
otelAttrs = otelAttrs
),
class = "ctx_otel_info"
)
}
with_otel_span_context <- function(otel_info, expr, domain) {
if (!otel_is_tracing_enabled()) {
return(force(expr))
}
isRecordingOtel <- .subset2(otel_info, "isRecordingOtel")
otelLabel <- .subset2(otel_info, "otelLabel")
otelAttrs <- .subset2(otel_info, "otelAttrs")
# Always set the reactive update span as active
# This ensures that any spans created within the reactive context
# are at least children of the reactive update span
maybe_with_otel_span_reactive_update(domain = domain, {
if (isRecordingOtel) {
with_otel_span(
otelLabel,
{
# Works with both sync and async expressions
# Needed for both observer and reactive contexts
hybrid_then(
expr,
on_failure = set_otel_exception_status_and_throw,
# Must upgrade the error object
tee = FALSE
)
},
# expr,
attributes = otelAttrs
)
} else {
force(expr)
}
})
}
#' @include graph.R
Context <- R6Class(
'Context',
@@ -33,11 +87,14 @@ Context <- R6Class(
.pid = NULL,
.weak = NULL,
.otel_info = NULL,
initialize = function(
domain, label='', type='other', prevId='',
reactId = rLog$noReactId,
id = .getReactiveEnvironment()$nextId(), # For dummy context
weak = FALSE
weak = FALSE,
otel_info = ctx_otel_info_obj()
) {
id <<- id
.label <<- label
@@ -47,17 +104,26 @@ Context <- R6Class(
.reactType <<- type
.weak <<- weak
rLog$createContext(id, label, type, prevId, domain)
if (!is.null(otel_info)) {
if (IS_SHINY_LOCAL_PKG) {
stopifnot(inherits(otel_info, "ctx_otel_info"))
}
.otel_info <<- otel_info
}
},
run = function(func) {
"Run the provided function under this context."
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(reactivePromiseDomain(), {
withReactiveDomain(.domain, {
captureStackTraces({
env <- .getReactiveEnvironment()
rLog$enter(.reactId, id, .reactType, .domain)
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
env$runWith(self, func)
with_otel_span_context(.otel_info, domain = .domain, {
captureStackTraces({
env <- .getReactiveEnvironment()
rLog$enter(.reactId, id, .reactType, .domain)
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
env$runWith(self, func)
})
})
})
})
@@ -231,7 +297,7 @@ wrapForContext <- function(func, ctx) {
}
reactivePromiseDomain <- function() {
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)

View File

@@ -45,6 +45,8 @@ createMockDomain <- function() {
callbacks <- Callbacks$new()
ended <- FALSE
domain <- new.env(parent = emptyenv())
domain$ns <- function(id) id
domain$token <- "mock-domain"
domain$onEnded <- function(callback) {
return(callbacks$register(callback))
}
@@ -95,7 +97,11 @@ getDefaultReactiveDomain <- function() {
#' @rdname domains
#' @export
withReactiveDomain <- function(domain, expr) {
promises::with_promise_domain(createVarPromiseDomain(.globals, "domain", domain), expr)
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(
createVarPromiseDomain(.globals, "domain", domain),
expr
)
}
#

View File

@@ -79,19 +79,26 @@ ReactiveVal <- R6Class(
dependents = NULL
),
public = list(
.isRecordingOtel = FALSE, # Needs to be set by Shiny
.otelLabel = NULL, # Needs to be set by Shiny
.otelAttrs = NULL, # Needs to be set by Shiny
initialize = function(value, label = NULL) {
reactId <- nextGlobalReactId()
private$reactId <- reactId
private$value <- value
private$label <- label
private$dependents <- Dependents$new(reactId = private$reactId)
rLog$define(private$reactId, value, private$label, type = "reactiveVal", getDefaultReactiveDomain())
domain <- getDefaultReactiveDomain()
rLog$define(private$reactId, value, private$label, type = "reactiveVal", domain)
.otelLabel <<- otel_log_label_set_reactive_val(private$label, domain = domain)
},
get = function() {
private$dependents$register()
if (private$frozen)
reactiveStop()
reactiveStop()
private$value
},
@@ -99,7 +106,16 @@ ReactiveVal <- R6Class(
if (identical(private$value, value)) {
return(invisible(FALSE))
}
rLog$valueChange(private$reactId, value, getDefaultReactiveDomain())
domain <- getDefaultReactiveDomain()
if ((!is.null(domain)) && .isRecordingOtel) {
otel_log(
.otelLabel,
severity = "info",
attributes = c(private$.otelAttrs, otel_session_id_attrs(domain))
)
}
rLog$valueChange(private$reactId, value, domain)
private$value <- value
private$dependents$invalidate()
invisible(TRUE)
@@ -205,13 +221,20 @@ ReactiveVal <- R6Class(
#'
#' @export
reactiveVal <- function(value = NULL, label = NULL) {
call_srcref <- get_call_srcref()
if (missing(label)) {
call <- sys.call()
label <- rvalSrcrefToLabel(attr(call, "srcref", exact = TRUE))
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = paste0("reactiveVal", createUniqueId(4))
)
}
rv <- ReactiveVal$new(value, label)
structure(
if (!is.null(call_srcref)) {
rv$.otelAttrs <- otel_srcref_attributes(call_srcref, fn_name = "reactiveVal")
}
ret <- structure(
function(x) {
if (missing(x)) {
rv$get()
@@ -224,6 +247,12 @@ reactiveVal <- function(value = NULL, label = NULL) {
label = label,
.impl = rv
)
if (has_otel_collect("reactivity")) {
ret <- enable_otel_reactive_val(ret)
}
ret
}
#' @rdname freezeReactiveValue
@@ -262,8 +291,11 @@ format.reactiveVal <- function(x, ...) {
# assigned to (e.g. for `a <- reactiveVal()`, the result should be "a"). This
# is a fragile, error-prone operation, so we default to a random label if
# necessary.
rvalSrcrefToLabel <- function(srcref,
defaultLabel = paste0("reactiveVal", createUniqueId(4))) {
rassignSrcrefToLabel <- function(
srcref,
defaultLabel,
fnName = "([a-zA-Z0-9_.]+)"
) {
if (is.null(srcref))
return(defaultLabel)
@@ -272,22 +304,29 @@ rvalSrcrefToLabel <- function(srcref,
if (is.null(srcfile))
return(defaultLabel)
if (is.null(srcfile$lines))
src <- getSrcfileLines(srcfile, srcref)
lines <- src$lines
line_num <- src$line_num
if (is.null(lines))
return(defaultLabel)
lines <- srcfile$lines
# When pasting at the Console, srcfile$lines is not split
if (length(lines) == 1) {
lines <- strsplit(lines, "\n")[[1]]
}
if (length(lines) < srcref[1]) {
if (length(lines) < line_num) {
return(defaultLabel)
}
firstLine <- substring(lines[srcref[1]], srcref[2] - 1)
firstLine <- substring(lines[line_num], srcref[2] - 1)
m <- regexec("\\s*([^[:space:]]+)\\s*(<-|=)\\s*reactiveVal\\b", firstLine)
m <- regexec(
# Require the first assignment within the line
paste0("^\\s*([^[:space:]]+)\\s*(<<-|<-|=)\\s*", fnName, "\\b"),
firstLine
)
if (m[[1]][1] == -1) {
return(defaultLabel)
}
@@ -330,6 +369,9 @@ ReactiveValues <- R6Class(
# object, but it does not preserve order.
.nameOrder = character(0),
.isRecordingOtel = FALSE, # Needs to be set by Shiny
.otelAttrs = NULL, # Needs to be set by Shiny
initialize = function(
dedupe = TRUE,
@@ -406,6 +448,21 @@ ReactiveValues <- R6Class(
return(invisible())
}
if ((!is.null(domain)) && .isRecordingOtel) {
if (
# Any reactiveValues (other than input or clientData) are fair game
!(.label == "input" || .label == "clientData") ||
# Do not include updates to input or clientData unless _some_ reactivity has occured
!is.null(domain$userData[["_otel_has_reactive_cleanup"]])
) {
otel_log(
otel_log_label_set_reactive_values(.label, key, domain = domain),
severity = "info",
attributes = c(.otelAttrs, otel_session_id_attrs(domain))
)
}
}
# If it's new, append key to the name order
if (!key_exists) {
.nameOrder[length(.nameOrder) + 1] <<- key
@@ -579,10 +636,28 @@ reactiveValues <- function(...) {
if ((length(args) > 0) && (is.null(names(args)) || any(names(args) == "")))
rlang::abort("All arguments passed to reactiveValues() must be named.")
values <- .createReactiveValues(ReactiveValues$new())
values <- .createReactiveValues(ReactiveValues$new(), withOtel = FALSE)
# Use .subset2() instead of [[, to avoid method dispatch
.subset2(values, 'impl')$mset(args)
impl <- .subset2(values, 'impl')
call_srcref <- get_call_srcref()
if (!is.null(call_srcref)) {
impl$.label <- rassignSrcrefToLabel(
call_srcref,
# Pass through the random default label created in ReactiveValues$new()
defaultLabel = impl$.label
)
impl$.otelAttrs <- otel_srcref_attributes(call_srcref, fn_name = "reactiveValues")
}
impl$mset(args)
# Add otel collection after `$mset()` so that we don't log the initial values
# Add otel collection after `.label` so that any logging uses the correct label
values <- maybeAddReactiveValuesOtel(values)
values
}
@@ -597,10 +672,11 @@ checkName <- function(x) {
# @param values A ReactiveValues object
# @param readonly Should this object be read-only?
# @param ns A namespace function (either `identity` or `NS(namespace)`)
# @param withOtel Should otel collection be attempted?
.createReactiveValues <- function(values = NULL, readonly = FALSE,
ns = identity) {
ns = identity, withOtel = TRUE) {
structure(
ret <- structure(
list(
impl = values,
readonly = readonly,
@@ -608,6 +684,20 @@ checkName <- function(x) {
),
class='reactivevalues'
)
if (withOtel) {
ret <- maybeAddReactiveValuesOtel(ret)
}
ret
}
maybeAddReactiveValuesOtel <- function(x) {
if (!has_otel_collect("reactivity")) {
return(x)
}
enable_otel_reactive_values(x)
}
#' @export
@@ -831,6 +921,10 @@ Observable <- R6Class(
.mostRecentCtxId = character(0),
.ctx = 'Context',
.isRecordingOtel = FALSE, # Needs to be set by Shiny
.otelLabel = NULL, # Needs to be set by Shiny
.otelAttrs = NULL, # Needs to be set by Shiny
initialize = function(func, label = deparse(substitute(func)),
domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE) {
@@ -885,9 +979,19 @@ Observable <- R6Class(
simpleExprToFunction(fn_body(.origFunc), "reactive")
},
.updateValue = function() {
ctx <- Context$new(.domain, .label, type = 'observable',
prevId = .mostRecentCtxId, reactId = .reactId,
weak = TRUE)
ctx <- Context$new(
.domain,
.label,
type = 'observable',
prevId = .mostRecentCtxId,
reactId = .reactId,
weak = TRUE,
otel_info = ctx_otel_info_obj(
isRecordingOtel = .isRecordingOtel,
otelLabel = .otelLabel,
otelAttrs = c(.otelAttrs, otel_session_id_attrs(.domain))
)
)
.mostRecentCtxId <<- ctx$id
# A Dependency object will have a weak reference to the context, which
@@ -920,6 +1024,15 @@ Observable <- R6Class(
},
error = function(cond) {
if (.isRecordingOtel) {
# `cond` is too early in the stack to be updated by `ctx`'s
# `with_otel_span_context()` where it calls
# `set_otel_exception_status_and_throw()` on eval error.
# So we mark it as seen here.
# When the error is re-thrown later, it won't be a _new_ error
cond <- mark_otel_exception_as_seen(cond)
}
# If an error occurs, we want to propagate the error, but we also
# want to save a copy of it, so future callers of this reactive will
# get the same error (i.e. the error is cached).
@@ -1017,12 +1130,24 @@ reactive <- function(
label <- exprToLabel(userExpr, "reactive", label)
o <- Observable$new(func, label, domain, ..stacktraceon = ..stacktraceon)
structure(
call_srcref <- get_call_srcref()
if (!is.null(call_srcref)) {
o$.otelAttrs <- otel_srcref_attributes(call_srcref, fn_name = "reactive")
}
ret <- structure(
o$getValue,
observable = o,
cacheHint = list(userExpr = zap_srcref(userExpr)),
class = c("reactiveExpr", "reactive", "function")
)
if (has_otel_collect("reactivity")) {
ret <- enable_otel_reactive_expr(ret)
}
ret
}
# Given the srcref to a reactive expression, attempts to figure out what the
@@ -1030,7 +1155,7 @@ reactive <- function(
# scans the line of code that started the reactive block and looks for something
# that looks like assignment. If we fail, fall back to a default value (likely
# the block of code in the body of the reactive).
rexprSrcrefToLabel <- function(srcref, defaultLabel) {
rexprSrcrefToLabel <- function(srcref, defaultLabel, fnName) {
if (is.null(srcref))
return(defaultLabel)
@@ -1038,22 +1163,26 @@ rexprSrcrefToLabel <- function(srcref, defaultLabel) {
if (is.null(srcfile))
return(defaultLabel)
if (is.null(srcfile$lines))
src <- getSrcfileLines(srcfile, srcref)
lines <- src$lines
line_num <- src$line_num
if (is.null(lines))
return(defaultLabel)
lines <- srcfile$lines
# When pasting at the Console, srcfile$lines is not split
if (length(lines) == 1) {
lines <- strsplit(lines, "\n")[[1]]
}
if (length(lines) < srcref[1]) {
if (length(lines) < line_num) {
return(defaultLabel)
}
firstLine <- substring(lines[srcref[1]], 1, srcref[2] - 1)
firstLine <- substring(lines[line_num], 1, srcref[2] - 1)
m <- regexec("(.*)(<-|=)\\s*reactive\\s*\\($", firstLine)
# Require the assignment to be parsed from the start
m <- regexec(paste0("^(.*)(<<-|<-|=)\\s*", fnName, "\\s*\\($"), firstLine)
if (m[[1]][1] == -1) {
return(defaultLabel)
}
@@ -1127,6 +1256,10 @@ Observer <- R6Class(
.prevId = character(0),
.ctx = NULL,
.isRecordingOtel = FALSE, # Needs to be set by Shiny
.otelLabel = NULL, # Needs to be set by Shiny
.otelAttrs = NULL, # Needs to be set by Shiny
initialize = function(observerFunc, label, suspended = FALSE, priority = 0,
domain = getDefaultReactiveDomain(),
autoDestroy = TRUE, ..stacktraceon = TRUE) {
@@ -1161,7 +1294,18 @@ Observer <- R6Class(
.createContext()$invalidate()
},
.createContext = function() {
ctx <- Context$new(.domain, .label, type='observer', prevId=.prevId, reactId = .reactId)
ctx <- Context$new(
.domain,
.label,
type = 'observer',
prevId = .prevId,
reactId = .reactId,
otel_info = ctx_otel_info_obj(
isRecordingOtel = .isRecordingOtel,
otelLabel = .otelLabel,
otelAttrs = c(.otelAttrs, otel_session_id_attrs(.domain))
)
)
.prevId <<- ctx$id
if (!is.null(.ctx)) {
@@ -1430,7 +1574,14 @@ observe <- function(
check_dots_empty()
func <- installExprFunction(x, "func", env, quoted)
label <- funcToLabel(func, "observe", label)
call_srcref <- get_call_srcref()
if (is.null(label)) {
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = funcToLabel(func, "observe", label)
)
}
o <- Observer$new(
func,
@@ -1441,6 +1592,14 @@ observe <- function(
autoDestroy = autoDestroy,
..stacktraceon = ..stacktraceon
)
if (!is.null(call_srcref)) {
o$.otelAttrs <- otel_srcref_attributes(call_srcref, fn_name = "observe")
}
if (has_otel_collect("reactivity")) {
o <- enable_otel_observe(o)
}
invisible(o)
}
@@ -1828,34 +1987,64 @@ coerceToFunc <- function(x) {
#' }
#' @export
reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
reactive_poll_impl(
fnName = "reactivePoll",
intervalMillis = intervalMillis,
session = session,
checkFunc = checkFunc,
valueFunc = valueFunc
)
}
reactive_poll_impl <- function(
fnName,
intervalMillis,
session,
checkFunc,
valueFunc
) {
intervalMillis <- coerceToFunc(intervalMillis)
rv <- reactiveValues(cookie = isolate(checkFunc()))
fnName <- match.arg(fnName, c("reactivePoll", "reactiveFileReader"), several.ok = FALSE)
call_srcref <- get_call_srcref(-1)
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = "<anonymous>",
fnName = fnName
)
re_finalized <- FALSE
env <- environment()
o <- observe({
# When no one holds a reference to the reactive returned from
# reactivePoll, destroy and remove the observer so that it doesn't keep
# firing and hold onto resources.
if (re_finalized) {
o$destroy()
rm(o, envir = env)
return()
}
with_no_otel_collect({
cookie <- reactiveVal(
isolate(checkFunc()),
label = sprintf("%s %s cookie", fnName, label)
)
rv$cookie <- checkFunc()
invalidateLater(intervalMillis(), session)
o <- observe({
# When no one holds a reference to the reactive returned from
# reactivePoll, destroy and remove the observer so that it doesn't keep
# firing and hold onto resources.
if (re_finalized) {
o$destroy()
rm(o, envir = env)
return()
}
cookie(checkFunc())
invalidateLater(intervalMillis(), session)
}, label = sprintf("%s %s cleanup", fnName, label))
})
# TODO: what to use for a label?
re <- reactive({
rv$cookie
re <- reactive(label = sprintf("%s %s", fnName, label), {
# Take a dependency on the cookie, so that when it changes, this
# reactive expression is invalidated.
cookie()
valueFunc()
}, label = NULL)
})
reg.finalizer(attr(re, "observable"), function(e) {
re_finalized <<- TRUE
@@ -1865,6 +2054,16 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
# reference to `re` and thus prevent it from getting GC'd.
on.exit(rm(re))
local({
impl <- attr(re, "observable", exact = TRUE)
impl$.otelLabel <-
if (fnName == "reactivePoll")
otel_label_reactive_poll(label, domain = impl$.domain)
else if (fnName == "reactiveFileReader")
otel_label_reactive_file_reader(label, domain = impl$.domain)
impl$.otelAttrs <- append_otel_srcref_attrs(impl$.otelAttrs, call_srcref, fn_name = fnName)
})
return(re)
}
@@ -1928,14 +2127,16 @@ reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...)
filePath <- coerceToFunc(filePath)
extraArgs <- list2(...)
reactivePoll(
intervalMillis, session,
function() {
reactive_poll_impl(
fnName = "reactiveFileReader",
intervalMillis = intervalMillis,
session = session,
checkFunc = function() {
path <- filePath()
info <- file.info(path)
return(paste(path, info$mtime, info$size))
},
function() {
valueFunc = function() {
do.call(readFunc, c(filePath(), extraArgs))
}
)
@@ -2017,6 +2218,8 @@ isolate <- function(expr) {
} else {
reactId <- rLog$noReactId
}
# Do not track otel spans for `isolate()`
ctx <- Context$new(getDefaultReactiveDomain(), '[isolate]', type='isolate', reactId = reactId)
on.exit(ctx$invalidate())
# Matching ..stacktraceon../..stacktraceoff.. pair
@@ -2295,26 +2498,41 @@ observeEvent <- function(eventExpr, handlerExpr,
eventQ <- exprToQuo(eventExpr, event.env, event.quoted)
handlerQ <- exprToQuo(handlerExpr, handler.env, handler.quoted)
label <- quoToLabel(eventQ, "observeEvent", label)
call_srcref <- get_call_srcref()
if (is.null(label)) {
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = quoToLabel(eventQ, "observeEvent", label)
)
}
handler <- inject(observe(
!!handlerQ,
label = label,
suspended = suspended,
priority = priority,
domain = domain,
autoDestroy = TRUE,
..stacktraceon = TRUE
))
with_no_otel_collect({
handler <- inject(observe(
!!handlerQ,
label = label,
suspended = suspended,
priority = priority,
domain = domain,
autoDestroy = TRUE,
..stacktraceon = TRUE
))
o <- inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
once = once,
label = label,
!!eventQ,
x = handler
))
o <- inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
once = once,
label = label,
!!eventQ,
x = handler
))
})
if (!is.null(call_srcref)) {
o$.otelAttrs <- otel_srcref_attributes(call_srcref, fn_name = "observeEvent")
}
if (has_otel_collect("reactivity")) {
o <- enable_otel_observe(o)
}
invisible(o)
}
@@ -2333,15 +2551,40 @@ eventReactive <- function(eventExpr, valueExpr,
eventQ <- exprToQuo(eventExpr, event.env, event.quoted)
valueQ <- exprToQuo(valueExpr, value.env, value.quoted)
label <- quoToLabel(eventQ, "eventReactive", label)
func <- installExprFunction(eventExpr, "func", event.env, event.quoted, wrappedWithLabel = FALSE)
# Attach a label and a reference to the original user source for debugging
userEventExpr <- fn_body(func)
invisible(inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
label = label,
!!eventQ,
x = reactive(!!valueQ, domain = domain, label = label)
)))
call_srcref <- get_call_srcref()
if (is.null(label)) {
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = exprToLabel(userEventExpr, "eventReactive", label)
)
}
with_no_otel_collect({
value_r <- inject(reactive(!!valueQ, domain = domain, label = label))
r <- inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
label = label,
!!eventQ,
x = value_r
))
})
if (!is.null(call_srcref)) {
impl <- attr(r, "observable", exact = TRUE)
impl$.otelAttrs <- otel_srcref_attributes(call_srcref, fn_name = "eventReactive")
}
if (has_otel_collect("reactivity")) {
r <- enable_otel_reactive_expr(r)
}
return(r)
}
isNullEvent <- function(value) {
@@ -2456,71 +2699,103 @@ isNullEvent <- function(value) {
#'
#' @export
debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomain()) {
# TODO: make a nice label for the observer(s)
# Do not bind OpenTelemetry spans for debounce reactivity internals,
# except for the eventReactive that is returned.
force(r)
force(millis)
call_srcref <- get_call_srcref()
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = "<anonymous>"
)
if (!is.function(millis)) {
origMillis <- millis
millis <- function() origMillis
}
v <- reactiveValues(
trigger = NULL,
when = NULL # the deadline for the timer to fire; NULL if not scheduled
)
with_no_otel_collect({
trigger <- reactiveVal(NULL, label = sprintf("debounce %s trigger", label))
# the deadline for the timer to fire; NULL if not scheduled
when <- reactiveVal(NULL, label = sprintf("debounce %s when", label))
# Responsible for tracking when r() changes.
firstRun <- TRUE
observe({
if (firstRun) {
# During the first run we don't want to set v$when, as this will kick off
# the timer. We only want to do that when we see r() change.
firstRun <<- FALSE
# Responsible for tracking when r() changes.
firstRun <- TRUE
observe(
label = sprintf("debounce %s tracker", label),
domain = domain,
priority = priority,
{
if (firstRun) {
# During the first run we don't want to set `when`, as this will kick
# off the timer. We only want to do that when we see `r()` change.
firstRun <<- FALSE
# Ensure r() is called only after setting firstRun to FALSE since r()
# may throw an error
try(r(), silent = TRUE)
return()
}
# This ensures r() is still tracked after firstRun
try(r(), silent = TRUE)
# Ensure r() is called only after setting firstRun to FALSE since r()
# may throw an error
try(r(), silent = TRUE)
return()
}
# This ensures r() is still tracked after firstRun
try(r(), silent = TRUE)
# The value (or possibly millis) changed. Start or reset the timer.
v$when <- getDomainTimeMs(domain) + millis()
}, label = "debounce tracker", domain = domain, priority = priority)
# The value (or possibly millis) changed. Start or reset the timer.
when(
getDomainTimeMs(domain) + millis()
)
}
)
# This observer is the timer. It rests until v$when elapses, then touches
# v$trigger.
observe({
if (is.null(v$when))
return()
# This observer is the timer. It rests until `when` elapses, then touches
# `trigger`.
observe(
label = sprintf("debounce %s timer", label),
domain = domain,
priority = priority,
{
if (is.null(when()))
return()
now <- getDomainTimeMs(domain)
if (now >= v$when) {
# Mod by 999999999 to get predictable overflow behavior
v$trigger <- isolate(v$trigger %||% 0) %% 999999999 + 1
v$when <- NULL
} else {
invalidateLater(v$when - now)
}
}, label = "debounce timer", domain = domain, priority = priority)
now <- getDomainTimeMs(domain)
if (now >= when()) {
# Mod by 999999999 to get predictable overflow behavior
trigger(
isolate(trigger() %||% 0) %% 999999999 + 1
)
when(NULL)
} else {
invalidateLater(when() - now)
}
}
)
})
# This is the actual reactive that is returned to the user. It returns the
# value of r(), but only invalidates/updates when v$trigger is touched.
er <- eventReactive(v$trigger, {
r()
}, label = "debounce result", ignoreNULL = FALSE, domain = domain)
# value of r(), but only invalidates/updates when `trigger` is touched.
er <- eventReactive(
{trigger()}, {r()},
label = sprintf("debounce %s result", label), ignoreNULL = FALSE, domain = domain
)
# Force the value of er to be immediately cached upon creation. It's very hard
# to explain why this observer is needed, but if you want to understand, try
# commenting it out and studying the unit test failure that results.
primer <- observe({
primer$destroy()
try(er(), silent = TRUE)
}, label = "debounce primer", domain = domain, priority = priority)
# Update the otel label
local({
er_impl <- attr(er, "observable", exact = TRUE)
er_impl$.otelLabel <- otel_label_debounce(label, domain = domain)
er_impl$.otelAttrs <- append_otel_srcref_attrs(er_impl$.otelAttrs, call_srcref, fn_name = "debounce")
})
with_no_otel_collect({
# Force the value of er to be immediately cached upon creation. It's very hard
# to explain why this observer is needed, but if you want to understand, try
# commenting it out and studying the unit test failure that results.
primer <- observe({
primer$destroy()
try(er(), silent = TRUE)
}, label = sprintf("debounce %s primer", label), domain = domain, priority = priority)
})
er
}
@@ -2528,69 +2803,88 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
#' @rdname debounce
#' @export
throttle <- function(r, millis, priority = 100, domain = getDefaultReactiveDomain()) {
# TODO: make a nice label for the observer(s)
# Do not bind OpenTelemetry spans for throttle reactivity internals,
# except for the eventReactive that is returned.
force(r)
force(millis)
call_srcref <- get_call_srcref()
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = "<anonymous>"
)
if (!is.function(millis)) {
origMillis <- millis
millis <- function() origMillis
}
v <- reactiveValues(
trigger = 0,
lastTriggeredAt = NULL, # Last time we fired; NULL if never
pending = FALSE # If TRUE, trigger again when timer elapses
)
with_no_otel_collect({
trigger <- reactiveVal(0, label = sprintf("throttle %s trigger", label))
# Last time we fired; NULL if never
lastTriggeredAt <- reactiveVal(NULL, label = sprintf("throttle %s last triggered at", label))
# If TRUE, trigger again when timer elapses
pending <- reactiveVal(FALSE, label = sprintf("throttle %s pending", label))
})
blackoutMillisLeft <- function() {
if (is.null(v$lastTriggeredAt)) {
if (is.null(lastTriggeredAt())) {
0
} else {
max(0, v$lastTriggeredAt + millis() - getDomainTimeMs(domain))
max(0, lastTriggeredAt() + millis() - getDomainTimeMs(domain))
}
}
trigger <- function() {
v$lastTriggeredAt <- getDomainTimeMs(domain)
update_trigger <- function() {
lastTriggeredAt(getDomainTimeMs(domain))
# Mod by 999999999 to get predictable overflow behavior
v$trigger <- isolate(v$trigger) %% 999999999 + 1
v$pending <- FALSE
trigger(isolate(trigger()) %% 999999999 + 1)
pending(FALSE)
}
# Responsible for tracking when f() changes.
observeEvent(try(r(), silent = TRUE), {
if (v$pending) {
# In a blackout period and someone already scheduled; do nothing
} else if (blackoutMillisLeft() > 0) {
# In a blackout period but this is the first change in that period; set
# v$pending so that a trigger will be scheduled at the end of the period
v$pending <- TRUE
} else {
# Not in a blackout period. Trigger, which will start a new blackout
# period.
trigger()
}
}, label = "throttle tracker", ignoreNULL = FALSE, priority = priority, domain = domain)
with_no_otel_collect({
# Responsible for tracking when f() changes.
observeEvent(try(r(), silent = TRUE), {
if (pending()) {
# In a blackout period and someone already scheduled; do nothing
} else if (blackoutMillisLeft() > 0) {
# In a blackout period but this is the first change in that period; set
# pending so that a trigger will be scheduled at the end of the period
pending(TRUE)
} else {
# Not in a blackout period. Trigger, which will start a new blackout
# period.
update_trigger()
}
}, label = sprintf("throttle %s tracker", label), ignoreNULL = FALSE, priority = priority, domain = domain)
observe({
if (!v$pending) {
return()
}
observe({
if (!pending()) {
return()
}
timeout <- blackoutMillisLeft()
if (timeout > 0) {
invalidateLater(timeout)
} else {
trigger()
}
}, priority = priority, domain = domain)
timeout <- blackoutMillisLeft()
if (timeout > 0) {
invalidateLater(timeout)
} else {
update_trigger()
}
}, label = sprintf("throttle %s trigger", label), priority = priority, domain = domain)
})
# This is the actual reactive that is returned to the user. It returns the
# value of r(), but only invalidates/updates when v$trigger is touched.
eventReactive(v$trigger, {
# value of r(), but only invalidates/updates when trigger is touched.
er <- eventReactive({trigger()}, {
r()
}, label = "throttle result", ignoreNULL = FALSE, domain = domain)
}, label = sprintf("throttle %s result", label), ignoreNULL = FALSE, domain = domain)
# Update the otel label
local({
er_impl <- attr(er, "observable", exact = TRUE)
er_impl$.otelLabel <- otel_label_throttle(label, domain = domain)
er_impl$.otelAttrs <- append_otel_srcref_attrs(er_impl$.otelAttrs, call_srcref, fn_name = "throttle")
})
er
}

View File

@@ -253,7 +253,7 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
hybrid_chain(
hybrid_chain(
promises::with_promise_domain(domain, {
with_promise_domain(domain, {
hybrid_chain(
func(),
function(value) {

View File

@@ -46,6 +46,12 @@
#' only used for recording or running automated tests. Defaults to the
#' `shiny.testmode` option, or FALSE if the option is not set.
#'
#' @return The value passed to [stopApp()], or throws an error if the app was
#' stopped with an error.
#'
#' @seealso [startApp()] for non-blocking mode, [stopApp()] to stop a running
#' app.
#'
#' @examples
#' \dontrun{
#' # Start app in the current working directory
@@ -84,18 +90,23 @@
#' runApp(app)
#' }
#' @export
runApp <- function(appDir=getwd(),
port=getOption('shiny.port'),
launch.browser = getOption('shiny.launch.browser', interactive()),
host=getOption('shiny.host', '127.0.0.1'),
workerId="", quiet=FALSE,
display.mode=c("auto", "normal", "showcase"),
test.mode=getOption('shiny.testmode', FALSE)) {
on.exit({
handlerManager$clear()
}, add = TRUE)
runApp <- function(
appDir=getwd(),
port=getOption('shiny.port'),
launch.browser = getOption('shiny.launch.browser', interactive()),
host=getOption('shiny.host', '127.0.0.1'),
workerId="", quiet=FALSE,
display.mode=c("auto", "normal", "showcase"),
test.mode=getOption('shiny.testmode', FALSE)
) {
# * Wrap **all** execution of the app inside the otel promise domain
# * While this could be done at a lower level, it allows for _anything_ within
# shiny's control to allow for the opportunity to have otel active spans be
# reactivated upon promise domain restoration
promises::local_otel_promise_domain()
if (isRunning()) {
# Check for nested blocking runApp() before sourcing app code
if (isRunning() && is.null(.globals$runningHandle)) {
stop("Can't call `runApp()` from within `runApp()`. If your ",
"application code contains `runApp()`, please remove it.")
}
@@ -107,14 +118,13 @@ runApp <- function(appDir=getwd(),
warn = max(1, getOption("warn", default = 1)),
pool.scheduler = scheduleTask
)
on.exit(options(ops), add = TRUE)
# ============================================================================
# Global onStart/onStop callbacks
# ============================================================================
# Invoke user-defined onStop callbacks, before the application's internal
# onStop callbacks.
on.exit({
# Ensure options are restored and onStop callbacks fire even if
# as.shiny.appobj() errors. Once .setupShinyApp() succeeds, the returned
# cleanup function takes over and this guard becomes a no-op.
setupComplete <- FALSE
on.exit(if (!setupComplete) {
options(ops)
.globals$onStopCallbacks$invoke()
.globals$onStopCallbacks <- Callbacks$new()
}, add = TRUE)
@@ -126,32 +136,140 @@ runApp <- function(appDir=getwd(),
# ============================================================================
appParts <- as.shiny.appobj(appDir)
# ============================================================================
# Initialize app state object
# ============================================================================
# This is so calls to getCurrentAppState() can be used to find (A) whether an
# app is running and (B), get options and data associated with the app.
initCurrentAppState(appParts)
on.exit(clearCurrentAppState(), add = TRUE)
# Any shinyOptions set after this point will apply to the current app only
# (and will not persist after the app stops).
result <- .setupShinyApp(
appDir, appParts, port, launch.browser, host,
workerId, quiet, display.mode, test.mode, ops = ops
)
setupComplete <- TRUE
on.exit(result$cleanup(), add = TRUE)
# ============================================================================
# shinyOptions
# Run event loop via httpuv
# ============================================================================
# A unique identifier associated with this run of this application. It is
# shared across sessions.
shinyOptions(appToken = createUniqueId(8))
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
# reactive(), Callbacks$invoke(), and others
..stacktraceoff..(
captureStackTraces({
while (!.globals$stopped) {
..stacktracefloor..(serviceApp())
}
})
)
# Set up default cache for app.
if (is.null(getShinyOption("cache", default = NULL))) {
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
if (isTRUE(.globals$reterror)) {
stop(.globals$retval)
} else if (.globals$retval$visible) {
.globals$retval$value
} else {
invisible(.globals$retval$value)
}
}
# Extract appOptions (which is a list) and store them as shinyOptions, for
# this app. (This is the only place we have to store settings that are
# accessible both the UI and server portion of the app.)
applyCapturedAppOptions(appParts$appOptions)
#' Start Shiny Application (Non-Blocking)
#'
#' Starts a Shiny application in non-blocking mode, returning a
#' `ShinyAppHandle` immediately while the app runs in the background.
#' The `later` event loop services the app, so the R console remains
#' available for interaction.
#'
#' @inheritParams runApp
#'
#' @return A `ShinyAppHandle` object with methods `stop()`, `status()`,
#' `url()`, and `result()`. The `status()` method returns `"running"`,
#' `"success"`, or `"error"`. The `result()` method throws an error if called
#' while running, or re-throws the error if the app stopped with an error.
#'
#' @examples
#' \dontrun{
#' # Start app in the background
#' handle <- startApp("myapp")
#'
#' # Check status
#' handle$status()
#' handle$url()
#'
#' # Stop the app
#' handle$stop()
#' }
#'
#' @seealso [runApp()] for blocking mode, [stopApp()] to stop a running app.
#' @export
startApp <- function(
appDir = getwd(),
port = getOption("shiny.port"),
launch.browser = getOption("shiny.launch.browser", interactive()),
host = getOption("shiny.host", "127.0.0.1"),
workerId = "",
quiet = FALSE,
display.mode = c("auto", "normal", "showcase"),
test.mode = getOption("shiny.testmode", FALSE)
) {
# OTEL: `local_otel_promise_domain()` ties its lifetime to this frame,
# which exits as soon as the handle is returned — before any request is
# served. A persistent global install would instead leak into unrelated
# user promises between ticks. Wrap the synchronous setup below (covers
# onStart) and each service iteration in `serviceNonBlocking()` (covers
# handlers and observers). The domain is dormant between ticks, so it
# stays out of user promises created at the console.
# Make warnings print immediately
# Set pool.scheduler to support pool package
ops <- options(
# Raise warn level to 1, but don't lower it
warn = max(1, getOption("warn", default = 1)),
pool.scheduler = scheduleTask
)
# Ensure options are restored and onStop callbacks fire even if
# as.shiny.appobj() errors. See matching guard in runApp().
setupComplete <- FALSE
on.exit(if (!setupComplete) {
options(ops)
.globals$onStopCallbacks$invoke()
.globals$onStopCallbacks <- Callbacks$new()
}, add = TRUE)
require(shiny)
result <- promises::with_otel_promise_domain({
appParts <- as.shiny.appobj(appDir)
.setupShinyApp(
appDir, appParts, port, launch.browser, host,
workerId, quiet, display.mode, test.mode, ops = ops
)
})
setupComplete <- TRUE
handle <- ShinyAppHandle$new(result$appUrl, result$cleanup)
.globals$runningHandle <- handle
serviceNonBlocking(handle, .globals$serviceGeneration)
handle
}
# Shared initialization for runApp() and startApp().
# Handles all app setup: options, state, httpuv server, browser launch, etc.
# Returns list(appUrl, cleanup) where cleanup() tears down the app.
# On setup failure, internal on.exit handlers clean up partial state.
.setupShinyApp <- function(appDir, appParts, port, launch.browser, host,
workerId, quiet, display.mode, test.mode, ops,
caller = parent.frame()) {
# Guard on.exit handlers with this flag so they only fire on setup failure.
# On success, cleanup responsibility is handed to the caller via the
# returned cleanup function.
cleanupOnExit <- TRUE
on.exit(if (cleanupOnExit) handlerManager$clear(), add = TRUE)
if (isRunning()) {
if (!is.null(.globals$runningHandle)) {
message("Stopping running Shiny app.")
.globals$runningHandle$stop()
} else {
stop("Can't start a new app while another is running. ",
"If your application code contains `runApp()` or `startApp()`, remove it. ",
"Otherwise, stop the current app first with stopApp().")
}
}
# ============================================================================
# runApp options set via shinyApp(options = list(...))
@@ -173,25 +291,55 @@ runApp <- function(appDir=getwd(),
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
#
# I tried to make this as compact and intuitive as possible,
# given that there are four distinct possibilities to check
# `missing()` runs in the caller's frame: with defaults on the outer
# formals, arguments are no longer missing by the time they reach here.
appOps <- appParts$options
findVal <- function(arg, default) {
if (arg %in% names(appOps)) appOps[[arg]] else default
}
if (evalq(missing(port), caller)) port <- findVal("port", port)
if (evalq(missing(launch.browser), caller)) launch.browser <- findVal("launch.browser", launch.browser)
if (evalq(missing(host), caller)) host <- findVal("host", host)
if (evalq(missing(quiet), caller)) quiet <- findVal("quiet", quiet)
if (evalq(missing(display.mode), caller)) display.mode <- findVal("display.mode", display.mode)
if (evalq(missing(test.mode), caller)) test.mode <- findVal("test.mode", test.mode)
if (missing(port))
port <- findVal("port", port)
if (missing(launch.browser))
launch.browser <- findVal("launch.browser", launch.browser)
if (missing(host))
host <- findVal("host", host)
if (missing(quiet))
quiet <- findVal("quiet", quiet)
if (missing(display.mode))
display.mode <- findVal("display.mode", display.mode)
if (missing(test.mode))
test.mode <- findVal("test.mode", test.mode)
on.exit(if (cleanupOnExit) options(ops), add = TRUE)
# ============================================================================
# Global onStart/onStop callbacks
# ============================================================================
on.exit(if (cleanupOnExit) {
.globals$onStopCallbacks$invoke()
.globals$onStopCallbacks <- Callbacks$new()
}, add = TRUE)
# ============================================================================
# Initialize app state object
# ============================================================================
# This is so calls to getCurrentAppState() can be used to find (A) whether an
# app is running and (B), get options and data associated with the app.
initCurrentAppState(appParts)
on.exit(if (cleanupOnExit) clearCurrentAppState(), add = TRUE)
# Any shinyOptions set after this point will apply to the current app only
# (and will not persist after the app stops).
# ============================================================================
# shinyOptions
# ============================================================================
# A unique identifier associated with this run of this application. It is
# shared across sessions.
shinyOptions(appToken = createUniqueId(8))
# Set up default cache for app.
if (is.null(getShinyOption("cache", default = NULL))) {
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
}
# Extract appOptions (which is a list) and store them as shinyOptions, for
# this app. (This is the only place we have to store settings that are
# accessible both the UI and server portion of the app.)
applyCapturedAppOptions(appParts$appOptions)
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
@@ -207,8 +355,14 @@ runApp <- function(appDir=getwd(),
# any valid version.
ver <- Sys.getenv('SHINY_SERVER_VERSION')
if (utils::compareVersion(ver, .shinyServerMinVersion) < 0) {
warning('Shiny Server v', .shinyServerMinVersion,
' or later is required; please upgrade!')
rlang::warn(c(
sprintf(
"Shiny Server v%s or later is required; please upgrade.",
.shinyServerMinVersion
),
"i" = "If you are not using Shiny Server, you are likely seeing this message because the `SHINY_PORT` environment variable is set in your environment.",
"i" = "Avoid using `SHINY_PORT` to prevent this warning."
))
}
}
@@ -271,7 +425,7 @@ runApp <- function(appDir=getwd(),
# If display mode is specified as an argument, apply it (overriding the
# value specified in DESCRIPTION, if any).
display.mode <- match.arg(display.mode)
display.mode <- match.arg(display.mode, c("auto", "normal", "showcase"))
if (display.mode == "normal") {
setShowcaseDefault(0)
}
@@ -325,24 +479,21 @@ runApp <- function(appDir=getwd(),
# onStart/onStop callbacks
# ============================================================================
# Set up the onStop before we call onStart, so that it gets called even if an
# error happens in onStart.
# error happens in onStart or later during startup.
if (!is.null(appParts$onStop))
on.exit(appParts$onStop(), add = TRUE)
on.exit(if (cleanupOnExit) appParts$onStop(), add = TRUE)
if (!is.null(appParts$onStart))
appParts$onStart()
# ============================================================================
# Start/stop httpuv app
# Start httpuv app
# ============================================================================
server <- startApp(appParts, port, host, quiet)
server <- startHttpuvApp(appParts, port, host, quiet)
# Make the httpuv server object accessible. Needed for calling
# addResourcePath while app is running.
shinyOptions(server = server)
on.exit({
stopServer(server)
}, add = TRUE)
on.exit(if (cleanupOnExit) stopServer(server), add = TRUE)
# ============================================================================
# Launch web browser
@@ -373,39 +524,52 @@ runApp <- function(appDir=getwd(),
# Application hooks
# ============================================================================
callAppHook("onAppStart", appUrl)
on.exit({
callAppHook("onAppStop", appUrl)
}, add = TRUE)
on.exit(if (cleanupOnExit) callAppHook("onAppStop", appUrl), add = TRUE)
# ============================================================================
# Run event loop via httpuv
# ============================================================================
# Initialize globals used by the event loop and stopApp()
.globals$reterror <- NULL
.globals$retval <- NULL
.globals$stopped <- FALSE
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
# reactive(), Callbacks$invoke(), and others
..stacktraceoff..(
captureStackTraces({
while (!.globals$stopped) {
..stacktracefloor..(serviceApp())
}
})
)
if (isTRUE(.globals$reterror)) {
stop(.globals$retval)
# Invalidate any stale non-blocking service loops from a previous app.
# Each app launch gets a fresh generation so old callbacks become no-ops.
.globals$serviceGeneration <- (.globals$serviceGeneration %||% 0L) + 1L
# Setup complete - disable on.exit cleanup, hand off to caller
cleanupOnExit <- FALSE
list(
appUrl = appUrl,
cleanup = .createCleanup(server, appParts, appUrl, ops)
)
}
# Consolidated cleanup function for app teardown
.createCleanup <- function(server, appParts, appUrl, ops) {
cleanedUp <- FALSE
function() {
if (cleanedUp) return()
cleanedUp <<- TRUE
.globals$stopped <- TRUE
.globals$runningHandle <- NULL
handlerManager$clear()
options(ops)
.globals$onStopCallbacks$invoke()
.globals$onStopCallbacks <- Callbacks$new()
clearCurrentAppState()
if (!is.null(appParts$onStop)) appParts$onStop()
stopServer(server)
callAppHook("onAppStop", appUrl)
}
else if (.globals$retval$visible)
.globals$retval$value
else
invisible(.globals$retval$value)
}
#' Stop the currently running Shiny app
#'
#' Stops the currently running Shiny app, returning control to the caller of
#' [runApp()].
#' [runApp()]. Despite the similar names, `stopApp()` is not the
#' counterpart of [startApp()] — it is the counterpart of [runApp()],
#' controlling its return value via `returnValue`.
#'
#' @param returnValue The value that should be returned from
#' [runApp()].

View File

@@ -274,15 +274,20 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
args <- argsForServerFunc(serverFunc, shinysession)
withReactiveDomain(shinysession, {
do.call(
# No corresponding ..stacktraceoff; the server func is pure
# user code
wrapFunctionLabel(appvars$server, "server",
..stacktraceon = TRUE
),
args
)
otel_span_session_start(domain = shinysession, {
do.call(
# No corresponding ..stacktraceoff; the server func is pure
# user code
wrapFunctionLabel(appvars$server, "server",
..stacktraceon = TRUE
),
args
)
})
})
})
},
update = {
@@ -382,7 +387,7 @@ removeSubApp <- function(path) {
handlerManager$removeWSHandler(path)
}
startApp <- function(appObj, port, host, quiet) {
startHttpuvApp <- function(appObj, port, host, quiet) {
appHandlers <- createAppHandlers(appObj$httpHandler, appObj$serverFuncSource)
handlerManager$addHandler(appHandlers$http, "/", tail = TRUE)
handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE)
@@ -474,9 +479,12 @@ startApp <- function(appObj, port, host, quiet) {
}
}
# Run an application that was created by \code{\link{startApp}}. This
# Run an application that was created by \code{\link{startHttpuvApp}}. This
# function should normally be called in a \code{while(TRUE)} loop.
serviceApp <- function() {
serviceApp <- function(
# rely on lazy evaluation for maximum efficiency
timeout = max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
) {
timerCallbacks$executeElapsed()
flushReact()
@@ -486,13 +494,58 @@ serviceApp <- function() {
# to keep the session responsive to user input
maxTimeout <- ifelse(interactive(), 100, 1000)
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
service(timeout)
flushReact()
flushPendingSessions()
}
# Non-blocking service loop using later callbacks.
# Uses 1ms delay between iterations to yield CPU for console interaction.
# The generation token (incremented on every runApp() call) ensures that when
# a new app starts, any stale service loop from a previous non-blocking app
# exits cleanly instead of continuing to run.
# Each iteration wraps `serviceApp()` in `with_otel_promise_domain()` so the
# OTEL domain is active while Shiny processes its own work — handlers,
# later callbacks, promise fulfillments — all executed synchronously inside
# `serviceApp()`. Span wrapping is attached at promise-registration time, so
# callbacks registered inside an iteration stay instrumented when they fire
# later. The domain is dormant between ticks, keeping it out of unrelated
# user promises created while the console is interactive.
serviceNonBlocking <- function(handle, generation) {
serviceLoop <- function() {
if (!identical(.globals$serviceGeneration, generation)) {
return(invisible())
}
if (!.globals$stopped) {
promises::with_otel_promise_domain(
..stacktraceoff..(
captureStackTraces(
tryCatch(
..stacktracefloor..(serviceApp(.shinyServiceDelaySecs * 1000)),
error = function(e) {
.globals$stopped <- TRUE
.globals$retval <- e
.globals$reterror <- TRUE
}
)
)
)
)
}
if (!identical(.globals$serviceGeneration, generation)) {
return(invisible())
}
if (!.globals$stopped) {
later::later(serviceLoop, delay = .shinyServiceDelaySecs)
} else {
handle$stop()
}
}
later::later(serviceLoop, delay = .shinyServiceDelaySecs)
}
.shinyServiceDelaySecs <- 0.001
.shinyServerMinVersion <- '0.3.4'
#' Check whether a Shiny application is running

View File

@@ -70,7 +70,7 @@ getShinyOption <- function(name, default = NULL) {
#'
#' You can customize the file patterns Shiny will monitor by setting the
#' shiny.autoreload.pattern option. For example, to monitor only `ui.R`:
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`.
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`.
#'
#' As mentioned above, Shiny no longer polls watched files for changes.
#' Instead, using \pkg{watcher}, Shiny is notified of file changes as they
@@ -160,6 +160,31 @@ getShinyOption <- function(name, default = NULL) {
# ' side devmode features. Currently the primary feature is the client-side
# ' error console.}
### end shiny.client_devmode
#' \item{shiny.otel.collect (defaults to `Sys.getenv("SHINY_OTEL_COLLECT",
#' "all")`)}{Determines how Shiny will interact with OpenTelemetry.
#'
#' Supported values:
#' * `"none"` - No Shiny OpenTelemetry tracing.
#' * `"session"` - Adds session start/end spans.
#' * `"reactive_update"` - Spans for any synchronous/asynchronous reactive
#' update. (Includes `"session"` features).
#' * `"reactivity"` - Spans for all reactive expressions and logs for setting
#' reactive vals and values. (Includes `"reactive_update"` features). This
#' option must be set when creating any reactive objects that should record
#' OpenTelemetry spans / logs. See [`withOtelCollect()`] and
#' [`localOtelCollect()`] for ways to set this option locally when creating
#' your reactive expressions.
#' * `"all"` - All Shiny OpenTelemetry tracing. Currently equivalent to
#' `"reactivity"`.
#'
#' This option is useful for debugging and profiling while in production. This
#' option will only be useful if the `otelsdk` package is installed and
#' `otel::is_tracing_enabled()` returns `TRUE`. Please have any OpenTelemetry
#' environment variables set before loading any relevant R packages.
#'
#' To set this option locally within a specific part of your Shiny
#' application, see [`withOtelCollect()`] and [`localOtelCollect()`].}
#' \item{shiny.otel.sanitize.errors (defaults to `TRUE`)}{If `TRUE`, fatal and unhandled errors will be sanitized before being sent to the OpenTelemetry backend. The default value of `TRUE` is set to avoid potentially sending sensitive information to the OpenTelemetry backend. If you want the full error message and stack trace to be sent to the OpenTelemetry backend, set this option to `FALSE` or use `safeError(e)`.}
#' }
#'
#'

View File

@@ -4,11 +4,12 @@
#' @importFrom lifecycle deprecated is_present
#' @importFrom grDevices dev.set dev.cur
#' @importFrom fastmap fastmap
#' @importFrom promises %...!%
#' @importFrom promises %...>%
#' @importFrom promises
#' promise promise_resolve promise_reject is.promising
#' as.promise
#' %...!% %...>%
#' as.promise is.promising is.promise
#' promise_resolve promise_reject
#' hybrid_then
#' with_promise_domain new_promise_domain
#' @importFrom rlang
#' quo enquo enquo0 as_function get_expr get_env new_function enquos
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject

View File

@@ -428,7 +428,7 @@ ShinySession <- R6Class(
stop("Nested calls to withCurrentOutput() are not allowed.")
}
promises::with_promise_domain(
with_promise_domain(
createVarPromiseDomain(private, "currentOutputName", name),
expr
)
@@ -1056,6 +1056,19 @@ ShinySession <- R6Class(
class(e) <- c("shiny.error.fatal", class(e))
}
# For fatal errors, always log.
# For non-fatal errors, only log if we haven't seen this error before.
if (close || !has_seen_otel_exception(e)) {
otel_log(
if (close) "Fatal error" else "Unhandled error",
severity = if (close) "fatal" else "error",
attributes = otel::as_attributes(list(
session.id = self$token,
error = get_otel_error_obj(e)
))
)
}
private$unhandledErrorCallbacks$invoke(e, onError = printError)
.globals$onUnhandledErrorCallbacks$invoke(e, onError = printError)
@@ -1073,7 +1086,9 @@ ShinySession <- R6Class(
}
# ..stacktraceon matches with the top-level ..stacktraceoff..
withReactiveDomain(self, {
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
otel_span_session_end(domain = self, {
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
})
})
},
isClosed = function() {
@@ -1142,7 +1157,8 @@ ShinySession <- R6Class(
attr(label, "srcref") <- srcref
attr(label, "srcfile") <- srcfile
obs <- observe(..stacktraceon = FALSE, {
# Do not bind this `observe()` call
obs <- with_no_otel_collect(observe(..stacktraceon = FALSE, {
private$sendMessage(recalculating = list(
name = name, status = 'recalculating'
@@ -1154,7 +1170,9 @@ ShinySession <- R6Class(
hybrid_chain(
{
private$withCurrentOutput(name, {
shinyCallingHandlers(func())
maybe_with_otel_span_reactive_update({
shinyCallingHandlers(func())
}, domain = self)
})
},
catch = function(cond) {
@@ -1179,9 +1197,7 @@ ShinySession <- R6Class(
} else {
if (isTRUE(getOption("show.error.messages"))) printError(cond)
if (getOption("shiny.sanitize.errors", FALSE)) {
cond <- simpleError(paste("An error has occurred. Check your",
"logs or contact the app author for",
"clarification."))
cond <- sanitized_error()
}
self$unhandledError(cond, close = FALSE)
invisible(structure(list(), class = "try-error", condition = cond))
@@ -1245,7 +1261,7 @@ ShinySession <- R6Class(
private$invalidatedOutputValues$set(name, value)
}
)
}, suspended=private$shouldSuspend(name), label=label)
}, suspended=private$shouldSuspend(name), label=label))
# If any output attributes were added to the render function attach
# them to observer.
@@ -2023,7 +2039,7 @@ ShinySession <- R6Class(
ext <- paste(".", ext, sep = "")
tmpdata <- tempfile(fileext = ext)
return(Context$new(getDefaultReactiveDomain(), '[download]')$run(function() {
promises::with_promise_domain(reactivePromiseDomain(), {
with_promise_domain(reactivePromiseDomain(), {
captureStackTraces({
self$incrementBusyCount()
hybrid_chain(
@@ -2195,6 +2211,8 @@ ShinySession <- R6Class(
if (private$busyCount == 0L) {
rLog$asyncStart(domain = self)
private$sendMessage(busy = "busy")
otel_span_reactive_update_init(domain = self)
}
private$busyCount <- private$busyCount + 1L
},
@@ -2216,6 +2234,8 @@ ShinySession <- R6Class(
private$startCycle()
}
})
otel_span_reactive_update_teardown(domain = self)
}
}
)
@@ -2723,3 +2743,10 @@ validate_session_object <- function(session, label = as.character(sys.call(sys.p
)
}
}
sanitized_error <- function() {
simpleError(paste("An error has occurred. Check your",
"logs or contact the app author for",
"clarification."))
}

View File

@@ -130,11 +130,20 @@ markRenderFunction <- function(
# stop warning from happening again for the same object
hasExecuted$set(TRUE)
}
if (is.null(formals(renderFunc))) renderFunc()
else renderFunc(...)
..stacktraceoff..(
if (is.null(formals(renderFunc))) renderFunc()
else renderFunc(...)
)
}
structure(
otelAttrs <-
otel_srcref_attributes(
attr(renderFunc, "wrappedFunc", exact = TRUE),
# Can't retrieve the render function used at this point, so just use NULL
fn_name = NULL
)
ret <- structure(
wrappedRenderFunc,
class = c("shiny.render.function", "function"),
outputFunc = uiFunc,
@@ -142,8 +151,15 @@ markRenderFunction <- function(
hasExecuted = hasExecuted,
cacheHint = cacheHint,
cacheWriteHook = cacheWriteHook,
cacheReadHook = cacheReadHook
cacheReadHook = cacheReadHook,
otelAttrs = otelAttrs
)
if (has_otel_collect("reactivity")) {
ret <- enable_otel_shiny_render_function(ret)
}
ret
}
#' @export
@@ -261,7 +277,7 @@ createRenderFunction <- function(
) {
renderFunc <- function(shinysession, name, ...) {
hybrid_chain(
func(),
..stacktraceon..(func()),
function(value) {
transform(value, shinysession, name, ...)
}
@@ -271,9 +287,7 @@ createRenderFunction <- function(
# Hoist func's wrappedFunc attribute into renderFunc, so that when we pass
# renderFunc on to markRenderFunction, it is able to find the original user
# function.
if (identical(cacheHint, "auto")) {
attr(renderFunc, "wrappedFunc") <- attr(func, "wrappedFunc", exact = TRUE)
}
attr(renderFunc, "wrappedFunc") <- attr(func, "wrappedFunc", exact = TRUE)
markRenderFunction(outputFunc, renderFunc, outputArgs, cacheHint,
cacheWriteHook, cacheReadHook)
@@ -321,7 +335,7 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
# Get relevant attributes from a render function object.
renderFunctionAttributes <- function(x) {
attrs <- c("outputFunc", "outputArgs", "hasExecuted", "cacheHint")
attrs <- c("outputFunc", "outputArgs", "hasExecuted", "cacheHint", "otelAttrs")
names(attrs) <- attrs
lapply(attrs, function(name) attr(x, name, exact = TRUE))
}
@@ -383,7 +397,7 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#' The corresponding HTML output tag should be `div` or `img` and have
#' the CSS class name `shiny-image-output`.
#'
#' @seealso
#' @seealso
#' * For more details on how the images are generated, and how to control
#' the output, see [plotPNG()].
#' * Use [outputOptions()] to set general output options for an image output.
@@ -616,7 +630,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
domain <- createRenderPrintPromiseDomain(width)
hybrid_chain(
{
promises::with_promise_domain(domain, func())
with_promise_domain(domain, ..stacktraceon..(func()))
},
function(value) {
res <- withVisible(value)
@@ -645,7 +659,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
createRenderPrintPromiseDomain <- function(width) {
f <- file()
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {
@@ -815,9 +829,9 @@ renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
#'
#' @seealso
#' * The download handler, like other outputs, is suspended (disabled) by
#' default for download buttons and links that are hidden. Use
#' [outputOptions()] to control this behavior, e.g. to set
#' `suspendWhenHidden = FALSE` if the download is initiated by
#' default for download buttons and links that are hidden. Use
#' [outputOptions()] to control this behavior, e.g. to set
#' `suspendWhenHidden = FALSE` if the download is initiated by
#' programmatically clicking on the download button using JavaScript.
#' @export
downloadHandler <- function(filename, content, contentType=NULL, outputArgs=list()) {
@@ -951,7 +965,7 @@ legacyRenderDataTable <- function(expr, options = NULL, searchDelay = 500,
options <- checkDT9(options)
res <- checkAsIs(options)
hybrid_chain(
func(),
..stacktraceon..(func()),
function(data) {
if (length(dim(data)) != 2) return() # expects a rectangular data object
if (is.data.frame(data)) data <- as.data.frame(data)

View File

@@ -33,13 +33,6 @@ showcaseHead <- function() {
deps <- list(
jqueryuiDependency(),
htmlDependency(
"showdown",
"0.3.1",
src = "www/shared/showdown/compressed",
package="shiny",
script = "showdown.js"
),
htmlDependency(
"highlight.js",
"6.2",
@@ -61,10 +54,11 @@ showcaseHead <- function() {
mdfile <- file.path.ci(getwd(), 'Readme.md')
html <- tagList(
if (file.exists(mdfile))
tags$script(type="text/markdown", id="showcase-markdown-content",
paste(readUTF8(mdfile), collapse="\n"))
else ""
if (file.exists(mdfile)) {
md_content <- paste(readUTF8(mdfile), collapse="\n")
md_html <- commonmark::markdown_html(md_content, extensions = TRUE)
tags$template(id="showcase-markdown-content", HTML(md_html))
} else ""
)
return(attachDependencies(html, deps))

View File

@@ -208,8 +208,10 @@ exprToLabel <- function(expr, function_name, label = NULL) {
if (is.null(label)) {
label <- rexprSrcrefToLabel(
srcref[[1]],
simpleExprToFunction(expr, function_name)
simpleExprToFunction(expr, function_name),
function_name
)
label <- as_default_label(label)
}
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
@@ -229,10 +231,12 @@ funcToLabelBody <- function(func) {
funcToLabel <- function(func, functionLabel, label = NULL) {
if (!is.null(label)) return(label)
sprintf(
'%s(%s)',
functionLabel,
funcToLabelBody(func)
as_default_label(
sprintf(
'%s(%s)',
functionLabel,
funcToLabelBody(func)
)
)
}
quoToLabelBody <- function(q) {
@@ -241,9 +245,19 @@ quoToLabelBody <- function(q) {
quoToLabel <- function(q, functionLabel, label = NULL) {
if (!is.null(label)) return(label)
sprintf(
'%s(%s)',
functionLabel,
quoToLabelBody(q)
as_default_label(
sprintf(
'%s(%s)',
functionLabel,
quoToLabelBody(q)
)
)
}
as_default_label <- function(x) {
class(x) <- c("default_label", class(x))
x
}
is_default_label <- function(x) {
inherits(x, "default_label")
}

View File

@@ -797,8 +797,8 @@ cachedFuncWithFile <- function(dir, file, func, case.sensitive = FALSE) {
last_autoreload <- 0
function(...) {
fname <- if (case.sensitive) {
file.path(dir, file)
fname <- if (case.sensitive) {
file.path(dir, file)
} else {
file.path.ci(dir, file)
}
@@ -1366,31 +1366,62 @@ tryNativeEncoding <- function(string) {
if (identical(enc2utf8(string2), string)) string2 else string
}
# similarly, try to source() a file with UTF-8
sourceUTF8 <- function(file, envir = globalenv()) {
lines <- readUTF8(file)
enc <- if (any(Encoding(lines) == 'UTF-8')) 'UTF-8' else 'unknown'
src <- srcfilecopy(file, lines, isFile = TRUE) # source reference info
# oddly, parse(file) does not work when file contains multibyte chars that
# **can** be encoded natively on Windows (might be a bug in base R); we
# rewrite the source code in a natively encoded temp file and parse it in this
# case (the source reference is still pointed to the original file, though)
if (isWindows() && enc == 'unknown') {
file <- tempfile(); on.exit(unlink(file), add = TRUE)
writeLines(lines, file)
}
exprs <- try(parse(file, keep.source = FALSE, srcfile = src, encoding = enc))
if (inherits(exprs, "try-error")) {
diagnoseCode(file)
stop("Error sourcing ", file)
maybeAnnotateSourceForArk <- function(file, lines) {
ark_annotate_source <- get0(".ark_annotate_source", baseenv())
if (is.null(ark_annotate_source)) {
return(lines)
}
# Wrap the exprs in first `{`, then ..stacktraceon..(). It's only really the
# ..stacktraceon..() that we care about, but the `{` is needed to make that
# possible.
exprs <- makeCall(`{`, exprs)
# Need to wrap exprs in a list because we want it treated as a single argument
exprs <- makeCall(..stacktraceon.., list(exprs))
file <- normalizePath(file, mustWork = TRUE, winslash = "/") # Just to be safe
uri <- paste0("file:///", sub("^/", "", file)) # Ark expects URIs
lines_str <- paste(lines, collapse = "\n")
tryCatch(
{
annotated <- ark_annotate_source(lines_str, uri)
if (!is.null(annotated)) {
lines <- strsplit(annotated, "\n", fixed = TRUE)[[1]]
}
},
error = function(cnd) {
rlang::warn("Can't inject breakpoints for Ark", parent = cnd)
}
)
lines
}
# similarly, try to source() a file with UTF-8
sourceUTF8 <- function(file, envir = globalenv()) {
file_norm <- normalizePath(file, mustWork = TRUE, winslash = "/")
lines <- readUTF8(file)
enc <- if (any(Encoding(lines) == 'UTF-8')) 'UTF-8' else 'unknown'
# Inject Ark annotations for breakpoints if available
lines <- maybeAnnotateSourceForArk(file, lines)
# Wrap in `..stacktraceon..({...})` using string manipulation before parsing,
# with a `#line` directive to map source references back to the original file
lines <- c(
"..stacktraceon..({",
sprintf('#line 1 "%s"', file_norm),
lines,
"})"
)
# Create a source file copy, i.e. an in-memory srcfile that contains all the
# code but refers to an original file
src <- srcfilecopy(file, lines, isFile = TRUE)
# Parse from our annotated lines
exprs <- tryCatch(
parse(text = lines, keep.source = FALSE, srcfile = src, encoding = enc),
error = function(cnd) {
diagnoseCode(file)
stop("Error sourcing ", file)
}
)
eval(exprs, envir)
}
@@ -1433,7 +1464,11 @@ URLencode <- function(value, reserved = FALSE) {
dateYMD <- function(date = NULL, argName = "value") {
if (!length(date)) return(NULL)
tryCatch({
res <- format(as.Date(date), "%Y-%m-%d")
if (inherits(date, "POSIXt")) {
res <- format(date, "%Y-%m-%d")
} else {
res <- format(as.Date(date), "%Y-%m-%d")
}
if (any(is.na(res))) stop()
date <- res
},
@@ -1525,7 +1560,7 @@ promise_chain <- function(promise, ..., catch = NULL, finally = NULL,
}
if (!is.null(domain)) {
promises::with_promise_domain(domain, do(), replace = replace)
with_promise_domain(domain, do(), replace = replace)
} else {
do()
}
@@ -1542,7 +1577,7 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
{
captureStackTraces({
result <- withVisible(force(expr))
if (promises::is.promising(result$value)) {
if (is.promising(result$value)) {
# Purposefully NOT including domain (nor replace), as we're already in
# the domain at this point
p <- promise_chain(valueWithVisible(result), ..., catch = catch, finally = finally)
@@ -1576,7 +1611,7 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
}
if (!is.null(domain)) {
promises::with_promise_domain(domain, do(), replace = replace)
with_promise_domain(domain, do(), replace = replace)
} else {
do()
}
@@ -1594,7 +1629,7 @@ createVarPromiseDomain <- function(env, name, value) {
force(name)
force(value)
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
function(...) {
orig <- env[[name]]

View File

@@ -1,2 +1,2 @@
# Generated by tools/updateBootstrapDatepicker.R; do not edit by hand
version_bs_date_picker <- "1.9.0"
version_bs_date_picker <- "1.10.0"

14
README-npm.md Normal file
View File

@@ -0,0 +1,14 @@
@posit/shiny
============
This npm package contains TypeScript type definitions for Shiny's client-side JavaScript libraries.
It does not include the Shiny framework itself, though that may change in the future.
[Shiny](https://github.com/rstudio/shiny) is a web application framework for both R and Python, developed by Posit PBC.
## Installation
```bash
npm install @posit/shiny
```

View File

@@ -61,8 +61,8 @@ We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.
## License
The shiny package as a whole is licensed under the GPLv3. See the [LICENSE](LICENSE) file for more details.
The shiny package as a whole is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
## R version support
Shiny is supported on the latest release version of R, as well as the previous four minor release versions of R. For example, if the latest release R version is 4.1, then that version is supported, as well as 4.0, 3.6, 3.5, and 3.4.
Shiny is supported on the latest release version of R, as well as the previous four minor release versions of R. For example, if the latest release R version is 4.3, then that version is supported, as well as 4.2, 4.1, 4.0, 3.6.

View File

@@ -1,40 +1,129 @@
## Comments
#### 2025-12-08
Test has been removed from CRAN checks.
Also added a couple bug fixes as found by users.
Please let me know if you need any further changes.
Thank you,
Carson
#### 2025-12-04
Error:
```
Check Details
Version: 1.12.0
Check: tests
Result: ERROR
Running testthat.R [100s/394s]
Running the tests in tests/testthat.R failed.
Complete output:
> library(testthat)
> library(shiny)
>
> test_check("shiny")
Saving _problems/test-timer-35.R
[ FAIL 1 | WARN 0 | SKIP 22 | PASS 1981 ]
══ Skipped tests (22) ══════════════════════════════════════════════════════════
• File system is not case-sensitive (1): 'test-app.R:36:5'
• I'm not sure of a great way to test this without timers. (1):
'test-test-server.R:216:3'
• Not testing in CI (1): 'test-devmode.R:17:3'
• On CRAN (18): 'test-actionButton.R:59:1', 'test-busy-indication.R:1:1',
'test-busy-indication.R:15:1', 'test-busy-indication.R:50:1',
'test-otel-error.R:1:1', 'test-otel-mock.R:1:1', 'test-pkgdown.R:3:3',
'test-reactivity.r:146:1', 'test-reactivity.r:1240:5',
'test-reactivity.r:1240:5', 'test-stacks-deep.R:93:1',
'test-stacks-deep.R:141:1', 'test-stacks.R:140:3', 'test-tabPanel.R:46:1',
'test-tabPanel.R:66:1', 'test-tabPanel.R:73:1', 'test-tabPanel.R:83:1',
'test-utils.R:177:3'
• {shinytest2} is not installed (1): 'test-test-shinyAppTemplate.R:2:1'
══ Failed tests ════════════════════════════════════════════════════════════════
── Failure ('test-timer.R:35:3'): Unscheduling works ───────────────────────────
Expected `timerCallbacks$.times` to be identical to `origTimes`.
Differences:
`attr(actual, 'row.names')` is an integer vector ()
`attr(expected, 'row.names')` is a character vector ()
[ FAIL 1 | WARN 0 | SKIP 22 | PASS 1981 ]
Error:
! Test failures.
Execution halted
```
#### 2025-12-03
```
Dear maintainer,
Please see the problems shown on
<https://cran.r-project.org/web/checks/check_results_shiny.html>.
Please correct before 2025-12-17 to safely retain your package on CRAN.
The CRAN Team
```
## `R CMD check` results:
0 errors | 0 warning | 1 note
```
─ checking CRAN incoming feasibility ... [7s/70s] NOTE (1m 9.5s)
Maintainer: Carson Sievert <carson@posit.co>
Days since last update: 5
```
## revdepcheck results
We checked 1278 reverse dependencies (1277 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of shiny.
We checked 1383 reverse dependencies (1376 from CRAN + 7 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package.
* We saw 2 new problems (NOTEs only)
* We failed to check 19 packages due to installation issues
* We saw 0 new problems
* We failed to check 31 packages
Issues with CRAN packages are summarised below.
### New problems
R CMD check displayed NOTEs for two packages, unrelated to changes in shiny.
* HH
checking installed package size ... NOTE
* PopED
checking installed package size ... NOTE
### Failed to check
* animalEKF
* AovBay
* Certara.VPCResults
* chipPCR
* AssumpSure
* boinet
* brms
* cheem
* ctsem
* dartR.sim
* diveR
* gap
* jsmodule
* detourr
* FAfA
* fio
* fitteR
* FossilSimShiny
* GDINA
* ggsem
* grandR
* hbsaems
* langevitour
* lavaan.shiny
* lcsm
* linkspotter
* loon.shiny
* robmedExtra
* MOsemiind
* MVN
* pandemonium
* polarisR
* RCTrep
* rstanarm
* SensMap
* Seurat
* shinyTempSignal
* Signac
* statsr
* semdrw
* shotGroups
* sphereML
* spinifex
* SurprisalAnalysis
* TestAnaAPP
* tidyvpc

108
eslint.config.mjs Normal file
View File

@@ -0,0 +1,108 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import prettier from "eslint-plugin-prettier";
import unicorn from "eslint-plugin-unicorn";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [{
ignores: ["**/*.d.ts"],
}, ...compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
), {
plugins: {
"@typescript-eslint": typescriptEslint,
prettier,
unicorn,
},
languageOptions: {
globals: {
...globals.browser,
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
parser: tsParser,
ecmaVersion: 2021,
sourceType: "module",
parserOptions: {
project: ["./tsconfig.json"],
},
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "error",
"default-case": ["error"],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double", "avoid-escape"],
semi: ["error", "always"],
"dot-location": ["error", "property"],
camelcase: ["off"],
"unicorn/filename-case": ["error", {
case: "camelCase",
}],
"@typescript-eslint/array-type": ["error", {
default: "array-simple",
readonly: "array-simple",
}],
"@typescript-eslint/consistent-indexed-object-style": ["error", "index-signature"],
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/naming-convention": ["error", {
selector: "default",
format: ["camelCase"],
}, {
selector: "method",
modifiers: ["private"],
format: ["camelCase"],
leadingUnderscore: "require",
}, {
selector: "method",
modifiers: ["protected"],
format: ["camelCase"],
leadingUnderscore: "require",
}, {
selector: "variable",
format: ["camelCase"],
trailingUnderscore: "forbid",
leadingUnderscore: "forbid",
}, {
selector: "parameter",
format: ["camelCase"],
trailingUnderscore: "allow",
leadingUnderscore: "forbid",
}, {
selector: ["enum", "enumMember"],
format: ["PascalCase"],
}, {
selector: "typeLike",
format: ["PascalCase"],
custom: {
regex: "(t|T)ype$",
match: false,
},
}],
},
}];

View File

@@ -1,2 +1,2 @@
/*! shiny 1.11.1.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating{min-height:var(--shiny-spinner-size, 32px)}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating:has(>*),[data-shiny-busy-spinners] .recalculating:empty{opacity:1}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:var(--_shiny-fade-opacity);transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.html-widget-output{visibility:inherit!important}[data-shiny-busy-spinners] .recalculating.html-widget-output>*{visibility:hidden}[data-shiny-busy-spinners] .recalculating.html-widget-output :after{visibility:visible}[data-shiny-busy-spinners] .recalculating.shiny-html-output:not(.shiny-table-output):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating:not(.shiny-html-output)):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating.shiny-table-output):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-14%;right:97%}45%{left:0%;right:14%}55%{left:14%;right:0%}to{left:97%;right:-14%}}.shiny-spinner-output-container{--shiny-spinner-size: 0px}

View File

@@ -1,5 +1,5 @@
.datepicker {
border-radius: 0.25rem;
border-radius: 3px;
direction: ltr;
}
@@ -30,7 +30,7 @@
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid rgba(0, 0, 0, 0.15);
border-bottom: 7px solid var(--bs-border-color-translucent);
border-top: 0;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
@@ -41,7 +41,7 @@
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-bottom: 6px solid var(--bs-body-bg);
border-top: 0;
position: absolute;
}
@@ -73,13 +73,13 @@
.datepicker-dropdown.datepicker-orient-top:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid rgba(0, 0, 0, 0.15);
border-top: 7px solid var(--bs-border-color-translucent);
}
.datepicker-dropdown.datepicker-orient-top:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid #fff;
border-top: 6px solid var(--bs-body-bg);
}
.datepicker table {
@@ -105,64 +105,64 @@
}
.datepicker table tr td.old, .datepicker table tr td.new {
color: #6c757d;
color: #707782;
}
.datepicker table tr td.day:hover, .datepicker table tr td.focused {
color: #000;
background: #e9e9ea;
background: #e8e9e9;
cursor: pointer;
}
.datepicker table tr td.disabled, .datepicker table tr td.disabled:hover {
background: none;
color: #6c757d;
color: #707782;
cursor: default;
}
.datepicker table tr td.highlighted {
color: #000;
background-color: #d1ecf1;
border-color: #83ccd9;
background-color: #cdf4fa;
border-color: #70e0f1;
border-radius: 0;
}
.datepicker table tr td.highlighted:focus, .datepicker table tr td.highlighted.focus {
color: #000;
background-color: #bcd4d9;
border-color: #6299a3;
background-color: #b9dce1;
border-color: #54a8b5;
}
.datepicker table tr td.highlighted:hover {
color: #000;
background-color: #697679;
border-color: #73b3bf;
background-color: #677a7d;
border-color: #63c5d4;
}
.datepicker table tr td.highlighted:active, .datepicker table tr td.highlighted.active {
color: #000;
background-color: #bcd4d9;
border-color: #73b3bf;
background-color: #b9dce1;
border-color: #63c5d4;
}
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted.focus:active, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted:active.focus, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
color: #000;
background-color: #adc4c8;
border-color: #6299a3;
background-color: #aacbd0;
border-color: #54a8b5;
}
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.highlighted:hover, fieldset[disabled] .datepicker table tr td.highlighted:focus, fieldset[disabled] .datepicker table tr td.highlighted.focus {
background-color: #d1ecf1;
border-color: #83ccd9;
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted[disabled].focus, fieldset[disabled] .datepicker table tr td.highlighted:hover, fieldset[disabled] .datepicker table tr td.highlighted:focus, fieldset[disabled] .datepicker table tr td.highlighted.focus {
background-color: #cdf4fa;
border-color: #70e0f1;
}
.datepicker table tr td.highlighted.focused {
background: #aadce5;
background: #9feaf5;
}
.datepicker table tr td.highlighted.disabled, .datepicker table tr td.highlighted.disabled:active {
background: #d1ecf1;
color: #6c757d;
background: #cdf4fa;
color: #707782;
}
.datepicker table tr td.today {
@@ -189,13 +189,13 @@
border-color: #e0a12d;
}
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today.focus:active, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today:active.focus, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
color: #000;
background-color: #d4b67f;
border-color: #bf8926;
}
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today.focus[disabled], fieldset[disabled] .datepicker table tr td.today:hover, fieldset[disabled] .datepicker table tr td.today:focus, fieldset[disabled] .datepicker table tr td.today.focus {
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today[disabled].focus, fieldset[disabled] .datepicker table tr td.today:hover, fieldset[disabled] .datepicker table tr td.today:focus, fieldset[disabled] .datepicker table tr td.today.focus {
background-color: #ffdb99;
border-color: #ffb733;
}
@@ -206,96 +206,96 @@
.datepicker table tr td.today.disabled, .datepicker table tr td.today.disabled:active {
background: #ffdb99;
color: #6c757d;
color: #707782;
}
.datepicker table tr td.range {
color: #000;
background-color: #e9e9ea;
border-color: #b5b5b8;
background-color: #e8e9e9;
border-color: #b4b7b7;
border-radius: 0;
}
.datepicker table tr td.range:focus, .datepicker table tr td.range.focus {
color: #000;
background-color: #d2d2d3;
border-color: #88888a;
background-color: #d1d2d2;
border-color: #878989;
}
.datepicker table tr td.range:hover {
color: #000;
background-color: #757575;
border-color: #9f9fa2;
background-color: #747575;
border-color: #9ea1a1;
}
.datepicker table tr td.range:active, .datepicker table tr td.range.active {
color: #000;
background-color: #d2d2d3;
border-color: #9f9fa2;
background-color: #d1d2d2;
border-color: #9ea1a1;
}
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range.focus:active, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range:active.focus, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
color: #000;
background-color: #c1c1c2;
border-color: #88888a;
background-color: #c1c1c1;
border-color: #878989;
}
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range.focus[disabled], fieldset[disabled] .datepicker table tr td.range:hover, fieldset[disabled] .datepicker table tr td.range:focus, fieldset[disabled] .datepicker table tr td.range.focus {
background-color: #e9e9ea;
border-color: #b5b5b8;
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range[disabled].focus, fieldset[disabled] .datepicker table tr td.range:hover, fieldset[disabled] .datepicker table tr td.range:focus, fieldset[disabled] .datepicker table tr td.range.focus {
background-color: #e8e9e9;
border-color: #b4b7b7;
}
.datepicker table tr td.range.focused {
background: #cfcfd1;
background: #ced0d0;
}
.datepicker table tr td.range.disabled, .datepicker table tr td.range.disabled:active {
background: #e9e9ea;
color: #6c757d;
background: #e8e9e9;
color: #707782;
}
.datepicker table tr td.range.highlighted {
color: #000;
background-color: #ddebee;
border-color: #99c3cc;
background-color: #dbeff2;
border-color: #90ced7;
}
.datepicker table tr td.range.highlighted:focus, .datepicker table tr td.range.highlighted.focus {
color: #000;
background-color: #c7d4d6;
border-color: #739299;
background-color: #c5d7da;
border-color: #6c9aa1;
}
.datepicker table tr td.range.highlighted:hover {
color: #000;
background-color: #6f7677;
border-color: #87acb4;
background-color: #6e7879;
border-color: #7fb5bd;
}
.datepicker table tr td.range.highlighted:active, .datepicker table tr td.range.highlighted.active {
color: #000;
background-color: #c7d4d6;
border-color: #87acb4;
background-color: #c5d7da;
border-color: #7fb5bd;
}
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted.focus:active, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted:active.focus, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
color: #000;
background-color: #b7c3c6;
border-color: #739299;
background-color: #b6c6c9;
border-color: #6c9aa1;
}
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.range.highlighted:hover, fieldset[disabled] .datepicker table tr td.range.highlighted:focus, fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
background-color: #ddebee;
border-color: #99c3cc;
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted[disabled].focus, fieldset[disabled] .datepicker table tr td.range.highlighted:hover, fieldset[disabled] .datepicker table tr td.range.highlighted:focus, fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
background-color: #dbeff2;
border-color: #90ced7;
}
.datepicker table tr td.range.highlighted.focused {
background: #bbd7dd;
background: #b6dee4;
}
.datepicker table tr td.range.highlighted.disabled, .datepicker table tr td.range.highlighted.disabled:active {
background: #ddebee;
color: #6c757d;
background: #dbeff2;
color: #707782;
}
.datepicker table tr td.range.today {
@@ -322,92 +322,92 @@
border-color: #d08d14;
}
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today.focus:active, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today:active.focus, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
color: #000;
background-color: #cba561;
border-color: #b17811;
}
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today.focus[disabled], fieldset[disabled] .datepicker table tr td.range.today:hover, fieldset[disabled] .datepicker table tr td.range.today:focus, fieldset[disabled] .datepicker table tr td.range.today.focus {
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today[disabled].focus, fieldset[disabled] .datepicker table tr td.range.today:hover, fieldset[disabled] .datepicker table tr td.range.today:focus, fieldset[disabled] .datepicker table tr td.range.today.focus {
background-color: #f4c775;
border-color: #eca117;
}
.datepicker table tr td.range.today.disabled, .datepicker table tr td.range.today.disabled:active {
background: #f4c775;
color: #6c757d;
color: #707782;
}
.datepicker table tr td.selected, .datepicker table tr td.selected.highlighted {
color: #fff;
background-color: #898b8d;
border-color: #6b6e71;
color: #000;
background-color: #878889;
border-color: #606060;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:focus, .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted:focus, .datepicker table tr td.selected.highlighted.focus {
color: #fff;
background-color: #959798;
border-color: #909295;
color: #000;
background-color: #7a7a7b;
border-color: #484848;
}
.datepicker table tr td.selected:hover, .datepicker table tr td.selected.highlighted:hover {
color: #fff;
background-color: #c4c5c6;
border-color: #7d7f82;
color: #000;
background-color: #444445;
border-color: #545454;
}
.datepicker table tr td.selected:active, .datepicker table tr td.selected.active, .datepicker table tr td.selected.highlighted:active, .datepicker table tr td.selected.highlighted.active {
color: #fff;
background-color: #959798;
border-color: #7d7f82;
color: #000;
background-color: #7a7a7b;
border-color: #545454;
}
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected.focus:active, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted.focus:active, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
color: #fff;
background-color: #9d9fa0;
border-color: #909295;
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected:active.focus, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted:active.focus, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
color: #000;
background-color: #707172;
border-color: #484848;
}
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected.focus[disabled], fieldset[disabled] .datepicker table tr td.selected:hover, fieldset[disabled] .datepicker table tr td.selected:focus, fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.selected.highlighted:hover, fieldset[disabled] .datepicker table tr td.selected.highlighted:focus, fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
background-color: #898b8d;
border-color: #6b6e71;
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected[disabled].focus, fieldset[disabled] .datepicker table tr td.selected:hover, fieldset[disabled] .datepicker table tr td.selected:focus, fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted[disabled].focus, fieldset[disabled] .datepicker table tr td.selected.highlighted:hover, fieldset[disabled] .datepicker table tr td.selected.highlighted:focus, fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
background-color: #878889;
border-color: #606060;
}
.datepicker table tr td.active, .datepicker table tr td.active.highlighted {
color: #fff;
background-color: #007bff;
border-color: #0277f4;
color: #ffffff;
background-color: #007bc2;
border-color: #0176ba;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:focus, .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted:focus, .datepicker table tr td.active.highlighted.focus {
color: #fff;
background-color: #1a88ff;
border-color: #4199f7;
color: #ffffff;
background-color: #1a88c8;
border-color: #4198cb;
}
.datepicker table tr td.active:hover, .datepicker table tr td.active.highlighted:hover {
color: #fff;
background-color: #80bdff;
border-color: #2087f5;
color: #ffffff;
background-color: #80bde1;
border-color: #1f86c2;
}
.datepicker table tr td.active:active, .datepicker table tr td.active.active, .datepicker table tr td.active.highlighted:active, .datepicker table tr td.active.highlighted.active {
color: #fff;
background-color: #1a88ff;
border-color: #2087f5;
color: #ffffff;
background-color: #1a88c8;
border-color: #1f86c2;
}
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active.focus:active, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted.focus:active, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
color: #fff;
background-color: #2b91ff;
border-color: #4199f7;
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active:active.focus, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted:active.focus, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
color: #ffffff;
background-color: #2b91cc;
border-color: #4198cb;
}
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active.focus[disabled], fieldset[disabled] .datepicker table tr td.active:hover, fieldset[disabled] .datepicker table tr td.active:focus, fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.active.highlighted:hover, fieldset[disabled] .datepicker table tr td.active.highlighted:focus, fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
background-color: #007bff;
border-color: #0277f4;
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active[disabled].focus, fieldset[disabled] .datepicker table tr td.active:hover, fieldset[disabled] .datepicker table tr td.active:focus, fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted[disabled].focus, fieldset[disabled] .datepicker table tr td.active.highlighted:hover, fieldset[disabled] .datepicker table tr td.active.highlighted:focus, fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
background-color: #007bc2;
border-color: #0176ba;
}
.datepicker table tr td span {
@@ -423,53 +423,53 @@
.datepicker table tr td span:hover, .datepicker table tr td span.focused {
color: #000;
background: #e9e9ea;
background: #e8e9e9;
}
.datepicker table tr td span.disabled, .datepicker table tr td span.disabled:hover {
background: none;
color: #6c757d;
color: #707782;
cursor: default;
}
.datepicker table tr td span.active, .datepicker table tr td span.active:hover, .datepicker table tr td span.active.disabled, .datepicker table tr td span.active.disabled:hover {
color: #fff;
background-color: #007bff;
border-color: #0277f4;
color: #ffffff;
background-color: #007bc2;
border-color: #0176ba;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover {
color: #fff;
background-color: #1a88ff;
border-color: #4199f7;
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active:hover.focus, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled:hover.focus {
color: #ffffff;
background-color: #1a88c8;
border-color: #4198cb;
}
.datepicker table tr td span.active:hover, .datepicker table tr td span.active:hover:hover, .datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:hover:hover {
color: #fff;
background-color: #80bdff;
border-color: #2087f5;
color: #ffffff;
background-color: #80bde1;
border-color: #1f86c2;
}
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled.active:hover {
color: #fff;
background-color: #1a88ff;
border-color: #2087f5;
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active:hover.active, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled:hover.active {
color: #ffffff;
background-color: #1a88c8;
border-color: #1f86c2;
}
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active.focus:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active.focus:hover:active, .datepicker table tr td span.active.active:hover:hover, .datepicker table tr td span.active.active:hover:focus, .datepicker table tr td span.active.active.focus:hover, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled.focus:active, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled.focus:hover:active, .datepicker table tr td span.active.disabled.active:hover:hover, .datepicker table tr td span.active.disabled.active:hover:focus, .datepicker table tr td span.active.disabled.active.focus:hover {
color: #fff;
background-color: #2b91ff;
border-color: #4199f7;
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active:active.focus, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active:hover:active.focus, .datepicker table tr td span.active:hover.active:hover, .datepicker table tr td span.active:hover.active:focus, .datepicker table tr td span.active:hover.active.focus, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled:active.focus, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled:hover:active.focus, .datepicker table tr td span.active.disabled:hover.active:hover, .datepicker table tr td span.active.disabled:hover.active:focus, .datepicker table tr td span.active.disabled:hover.active.focus {
color: #ffffff;
background-color: #2b91cc;
border-color: #4198cb;
}
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active.focus[disabled], fieldset[disabled] .datepicker table tr td span.active:hover, fieldset[disabled] .datepicker table tr td span.active:focus, fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active.disabled:hover:hover, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover, .datepicker table tr td span.active[disabled]:hover:hover, .datepicker table tr td span.active[disabled]:hover:focus, .datepicker table tr td span.active.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active:hover:hover, fieldset[disabled] .datepicker table tr td span.active:hover:focus, fieldset[disabled] .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled.focus[disabled], fieldset[disabled] .datepicker table tr td span.active.disabled:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled.disabled:hover:hover, .datepicker table tr td span.active.disabled.disabled:hover:focus, .datepicker table tr td span.active.disabled.disabled.focus:hover, .datepicker table tr td span.active.disabled[disabled]:hover:hover, .datepicker table tr td span.active.disabled[disabled]:hover:focus, .datepicker table tr td span.active.disabled.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus:hover {
background-color: #007bff;
border-color: #0277f4;
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active[disabled].focus, fieldset[disabled] .datepicker table tr td span.active:hover, fieldset[disabled] .datepicker table tr td span.active:focus, fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover.disabled:hover, .datepicker table tr td span.active:hover.disabled:focus, .datepicker table tr td span.active:hover.disabled.focus, .datepicker table tr td span.active:hover[disabled]:hover, .datepicker table tr td span.active:hover[disabled]:focus, .datepicker table tr td span.active:hover[disabled].focus, fieldset[disabled] .datepicker table tr td span.active:hover:hover, fieldset[disabled] .datepicker table tr td span.active:hover:focus, fieldset[disabled] .datepicker table tr td span.active:hover.focus, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled[disabled].focus, fieldset[disabled] .datepicker table tr td span.active.disabled:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover.disabled:hover, .datepicker table tr td span.active.disabled:hover.disabled:focus, .datepicker table tr td span.active.disabled:hover.disabled.focus, .datepicker table tr td span.active.disabled:hover[disabled]:hover, .datepicker table tr td span.active.disabled:hover[disabled]:focus, .datepicker table tr td span.active.disabled:hover[disabled].focus, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus, fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus {
background-color: #007bc2;
border-color: #0176ba;
}
.datepicker table tr td span.old, .datepicker table tr td span.new {
color: #6c757d;
color: #707782;
}
.datepicker .datepicker-switch {
@@ -488,7 +488,7 @@
.datepicker .next:hover,
.datepicker tfoot tr th:hover {
color: #000;
background: #e9e9ea;
background: #e8e9e9;
}
.datepicker .prev.disabled, .datepicker .next.disabled {

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
/*!
* Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker)
* Datepicker for Bootstrap v1.10.0 (https://github.com/uxsolutions/bootstrap-datepicker)
*
* Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* Licensed under the Apache License v2.0 (https://www.apache.org/licenses/LICENSE-2.0)
*/
(function(factory){
@@ -61,7 +61,7 @@
replace: function(new_array){
if (!new_array)
return;
if (!$.isArray(new_array))
if (!Array.isArray(new_array))
new_array = [new_array];
this.clear();
this.push.apply(this, new_array);
@@ -103,9 +103,15 @@
this.isInput = this.element.is('input');
this.inputField = this.isInput ? this.element : this.element.find('input');
this.component = this.element.hasClass('date') ? this.element.find('.add-on, .input-group-addon, .input-group-append, .input-group-prepend, .btn') : false;
if (this.component && this.component.length === 0)
if (this.component && this.component.length === 0){
this.component = false;
this.isInline = !this.component && this.element.is('div');
}
if (this.o.isInline === null){
this.isInline = !this.component && !this.isInput;
} else {
this.isInline = this.o.isInline;
}
this.picker = $(DPGlobal.template);
@@ -176,7 +182,7 @@
},
_resolveDaysOfWeek: function(daysOfWeek){
if (!$.isArray(daysOfWeek))
if (!Array.isArray(daysOfWeek))
daysOfWeek = daysOfWeek.split(/[,\s]*/);
return $.map(daysOfWeek, Number);
},
@@ -263,7 +269,7 @@
o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted||[]);
o.datesDisabled = o.datesDisabled||[];
if (!$.isArray(o.datesDisabled)) {
if (!Array.isArray(o.datesDisabled)) {
o.datesDisabled = o.datesDisabled.split(',');
}
o.datesDisabled = $.map(o.datesDisabled, function(d){
@@ -570,16 +576,15 @@
clearDates: function(){
this.inputField.val('');
this.update();
this._trigger('changeDate');
this.update();
if (this.o.autoclose) {
this.hide();
}
},
setDates: function(){
var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
var args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
this.update.apply(this, args);
this._trigger('changeDate');
this.setValue();
@@ -587,7 +592,7 @@
},
setUTCDates: function(){
var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
var args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
this.setDates.apply(this, $.map(args, this._utc_to_local));
return this;
},
@@ -1039,7 +1044,7 @@
//Check if uniqueSort exists (supported by jquery >=1.12 and >=2.2)
//Fallback to unique function for older jquery versions
if ($.isFunction($.uniqueSort)) {
if (typeof $.uniqueSort === "function") {
clsName = $.uniqueSort(clsName);
} else {
clsName = $.unique(clsName);
@@ -1571,12 +1576,12 @@
if (new_date < this.dates[j]){
// Date being moved earlier/left
while (j >= 0 && new_date < this.dates[j]){
while (j >= 0 && new_date < this.dates[j] && (this.pickers[j].element.val() || "").length > 0) {
this.pickers[j--].setUTCDate(new_date);
}
} else if (new_date > this.dates[k]){
// Date being moved later/right
while (k < l && new_date > this.dates[k]){
while (k < l && new_date > this.dates[k] && (this.pickers[k].element.val() || "").length > 0) {
this.pickers[k++].setUTCDate(new_date);
}
}
@@ -1690,6 +1695,7 @@
endDate: Infinity,
forceParse: true,
format: 'mm/dd/yyyy',
isInline: null,
keepEmptyValues: false,
keyboardNavigation: true,
language: 'en',
@@ -2007,7 +2013,7 @@
/* DATEPICKER VERSION
* =================== */
$.fn.datepicker.version = '1.9.0';
$.fn.datepicker.version = '1.10.0';
$.fn.datepicker.deprecated = function(msg){
var console = window.console;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates["ar-DZ"]={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0,monthsTitle:"أشهر",clear:"إزالة",format:"yyyy/mm/dd",weekStart:0}}(jQuery);

View File

@@ -1 +1 @@
!function(a){a.fn.datepicker.dates.ca={days:["Diumenge","Dilluns","Dimarts","Dimecres","Dijous","Divendres","Dissabte"],daysShort:["Diu","Dil","Dmt","Dmc","Dij","Div","Dis"],daysMin:["dg","dl","dt","dc","dj","dv","ds"],months:["Gener","Febrer","Març","Abril","Maig","Juny","Juliol","Agost","Setembre","Octubre","Novembre","Desembre"],monthsShort:["Gen","Feb","Mar","Abr","Mai","Jun","Jul","Ago","Set","Oct","Nov","Des"],today:"Avui",monthsTitle:"Mesos",clear:"Esborrar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);
!function(a){a.fn.datepicker.dates.ca={days:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],daysShort:["dg.","dl.","dt.","dc.","dj.","dv.","ds."],daysMin:["dg","dl","dt","dc","dj","dv","ds"],months:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],monthsShort:["gen.","febr.","març","abr.","maig","juny","jul.","ag.","set.","oct.","nov.","des."],today:"Avui",monthsTitle:"Mesos",clear:"Esborra",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

View File

@@ -1 +1 @@
!function(a){a.fn.datepicker.dates.de={days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],daysShort:["Son","Mon","Die","Mit","Don","Fre","Sam"],daysMin:["So","Mo","Di","Mi","Do","Fr","Sa"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",monthsTitle:"Monate",clear:"Löschen",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);
!function(a){a.fn.datepicker.dates.de={days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],daysShort:["So","Mo","Di","Mi","Do","Fr","Sa"],daysMin:["So","Mo","Di","Mi","Do","Fr","Sa"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",monthsTitle:"Monate",clear:"Löschen",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);

View File

@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates["en-US"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"m/d/yyyy"}}(jQuery);

View File

@@ -1 +1 @@
!function(a){a.fn.datepicker.dates.fi={days:["sunnuntai","maanantai","tiistai","keskiviikko","torstai","perjantai","lauantai"],daysShort:["sun","maa","tii","kes","tor","per","lau"],daysMin:["su","ma","ti","ke","to","pe","la"],months:["tammikuu","helmikuu","maaliskuu","huhtikuu","toukokuu","kesäkuu","heinäkuu","elokuu","syyskuu","lokakuu","marraskuu","joulukuu"],monthsShort:["tam","hel","maa","huh","tou","kes","hei","elo","syy","lok","mar","jou"],today:"tänään",clear:"Tyhjennä",weekStart:1,format:"d.m.yyyy"}}(jQuery);
!function(a){a.fn.datepicker.dates.fi={days:["sunnuntai","maanantai","tiistai","keskiviikko","torstai","perjantai","lauantai"],daysShort:["sun","maa","tii","kes","tor","per","lau"],daysMin:["su","ma","ti","ke","to","pe","la"],months:["tammikuu","helmikuu","maaliskuu","huhtikuu","toukokuu","kesäkuu","heinäkuu","elokuu","syyskuu","lokakuu","marraskuu","joulukuu"],monthsShort:["tammi","helmi","maalis","huhti","touko","kesä","hei","elo","syys","loka","marras","joulu"],today:"tänään",clear:"Tyhjennä",weekStart:1,format:"d.m.yyyy"}}(jQuery);

View File

@@ -1 +1 @@
!function(a){a.fn.datepicker.dates.id={days:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],daysShort:["Mgu","Sen","Sel","Rab","Kam","Jum","Sab"],daysMin:["Mg","Sn","Sl","Ra","Ka","Ju","Sa"],months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ags","Sep","Okt","Nov","Des"],today:"Hari Ini",clear:"Kosongkan"}}(jQuery);
!function(a){a.fn.datepicker.dates.id={days:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],daysShort:["Min","Sen","Sel","Rab","Kam","Jum","Sab"],daysMin:["Mg","Sn","Sl","Rb","Km","Jm","Sb"],months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agt","Sep","Okt","Nov","Des"],today:"Hari Ini",monthsTitle:"Bulan",clear:"Kosongkan",weekStart:0,format:"dd-mm-yyyy"}}(jQuery);

View File

@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.mar={days:["रविवार","सोमवार","मंगळवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],daysShort:["रवि","सोम","मंगळ","बुध","गुरु","शुक्र","शनि"],daysMin:["र","सो","मं","बु","गु","शु","श"],months:["जानेवारी","फेब्रुवारी","मार्च","एप्रिल","मे","जून","जुलै","ऑगस्ट","सप्टेंबर","ऑक्टोबर","नोव्हेंबर","डिसेंबर"],monthsShort:["जाने.","फेब्रु.","मार्च","एप्रिल","मे","जून","जुलै","ऑगस्ट","सप्टें.","ऑक्टो.","नोव्हें.","डिसें."],today:"आज",monthsTitle:"महीने",clear:"हटवा",weekStart:1,format:"dd / mm / yyyy"}}(jQuery);

View File

@@ -1 +1 @@
!function(a){a.fn.datepicker.dates.uk={days:["Неділя","Понеділок","Вівторок","Середа","Четвер","П'ятниця","Субота"],daysShort:["Нед","Пнд","Втр","Срд","Чтв","Птн","Суб"],daysMin:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],months:["Cічень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],monthsShort:["Січ","Лют","Бер","Кві","Тра","Чер","Лип","Сер","Вер","Жов","Лис","Гру"],today:"Сьогодні",clear:"Очистити",format:"dd.mm.yyyy",weekStart:1}}(jQuery);
!function(a){a.fn.datepicker.dates.uk={days:["Неділя","Понеділок","Вівторок","Середа","Четвер","П'ятниця","Субота"],daysShort:["Нед","Пнд","Втр","Срд","Чтв","Птн","Суб"],daysMin:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],months:["Січень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],monthsShort:["Січ","Лют","Бер","Кві","Тра","Чер","Лип","Сер","Вер","Жов","Лис","Гру"],today:"Сьогодні",clear:"Очистити",format:"dd.mm.yyyy",weekStart:1}}(jQuery);

View File

@@ -1 +1 @@
!function(a){a.fn.datepicker.dates["zh-TW"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["週日","週一","週二","週三","週四","週五","週六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",format:"yyyymmdd",weekStart:1,clear:"清除"}}(jQuery);
!function(a){a.fn.datepicker.dates["zh-TW"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["週日","週一","週二","週三","週四","週五","週六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",monthsTitle:"月份",format:"yyyy/mm/dd",weekStart:0,titleFormat:"yyyy年mm月",clear:"清除"}}(jQuery);

File diff suppressed because one or more lines are too long

View File

@@ -31,7 +31,6 @@ Michael Bensoussan <mickey@seesmic.com>
Louis-Rémi Babé <lrbabe@gmail.com>
Robert Katić <robert.katic@gmail.com>
Damian Janowski <damian.janowski@gmail.com>
Anton Kovalyov <anton@kovalyov.net>
Dušan B. Jovanovic <dbjdbj@gmail.com>
Earle Castledine <mrspeaker@gmail.com>
Rich Dougherty <rich@rd.gen.nz>
@@ -69,11 +68,11 @@ temp01 <temp01irc@gmail.com>
Colin Snover <github.com@zetafleet.com>
Jared Grippe <jared@deadlyicon.com>
Ryan W Tenney <ryan@10e.us>
Alex Sexton <AlexSexton@gmail.com>
Pinhook <contact@pinhooklabs.com>
Ron Otten <r.j.g.otten@gmail.com>
Jephte Clain <Jephte.Clain@univ-reunion.fr>
Anton Matzneller <obhvsbypqghgc@gmail.com>
Alex Sexton <AlexSexton@gmail.com>
Dan Heberden <danheberden@gmail.com>
Henri Wiechers <hwiechers@gmail.com>
Russell Holbrook <russell.holbrook@patch.com>
@@ -87,9 +86,10 @@ Sylvester Keil <sylvester@keil.or.at>
Brandon Sterne <bsterne@mozilla.com>
Mathias Bynens <mathias@qiwi.be>
Lee Carpenter <elcarpie@gmail.com>
Timmy Willison <4timmywil@gmail.com>
Timmy Willison <timmywil@users.noreply.github.com>
Corey Frang <gnarf37@gmail.com>
Digitalxero <digitalxero>
Anton Kovalyov <anton@kovalyov.net>
David Murdoch <david@davidmurdoch.com>
Josh Varner <josh.varner@gmail.com>
Charles McNulty <cmcnulty@kznf.com>
@@ -111,7 +111,7 @@ Mike Sherov <mike.sherov@gmail.com>
Greg Hazel <ghazel@gmail.com>
Schalk Neethling <schalk@ossreleasefeed.com>
Denis Knauf <Denis.Knauf@gmail.com>
Timo Tijhof <krinklemail@gmail.com>
Timo Tijhof <krinkle@fastmail.com>
Steen Nielsen <swinedk@gmail.com>
Anton Ryzhov <anton@ryzhov.me>
Shi Chuan <shichuanr@gmail.com>
@@ -151,7 +151,6 @@ Chris Faulkner <thefaulkner@gmail.com>
Marcel Greter <marcel.greter@ocbnet.ch>
Elijah Manor <elijah.manor@gmail.com>
Daniel Chatfield <chatfielddaniel@gmail.com>
Daniel Gálvez <dgalvez@editablething.com>
Nikita Govorov <nikita.govorov@gmail.com>
Wesley Walser <waw325@gmail.com>
Mike Pennisi <mike@mikepennisi.com>
@@ -162,9 +161,7 @@ Dave Riddle <david@joyvuu.com>
Callum Macrae <callum@lynxphp.com>
Jonathan Sampson <jjdsampson@gmail.com>
Benjamin Truyman <bentruyman@gmail.com>
Jay Merrifield <fracmak@gmail.com>
James Huston <james@jameshuston.net>
Sai Lung Wong <sai.wong@huffingtonpost.com>
Erick Ruiz de Chávez <erickrdch@gmail.com>
David Bonner <dbonner@cogolabs.com>
Allen J Schmidt Jr <cobrasoft@gmail.com>
@@ -174,8 +171,11 @@ Ismail Khair <ismail.khair@gmail.com>
Carl Danley <carldanley@gmail.com>
Mike Petrovich <michael.c.petrovich@gmail.com>
Greg Lavallee <greglavallee@wapolabs.com>
Daniel Gálvez <dgalvez@editablething.com>
Sai Lung Wong <sai.wong@huffingtonpost.com>
Tom H Fuertes <TomFuertes@gmail.com>
Roland Eckl <eckl.roland@googlemail.com>
Jay Merrifield <fracmak@gmail.com>
Yiming He <yiminghe@gmail.com>
David Fox <dfoxinator@gmail.com>
Bennett Sorbo <bsorbo@gmail.com>
@@ -192,9 +192,9 @@ Diego Tres <diegotres@gmail.com>
Jean Boussier <jean.boussier@gmail.com>
Andrew Plummer <plummer.andrew@gmail.com>
Mark Raddatz <mraddatz@gmail.com>
Pascal Borreli <pascal@borreli.com>
Isaac Z. Schlueter <i@izs.me>
Karl Sieburg <ksieburg@yahoo.com>
Pascal Borreli <pascal@borreli.com>
Nguyen Phuc Lam <ruado1987@gmail.com>
Dmitry Gusev <dmitry.gusev@gmail.com>
Steven Benner <admin@stevenbenner.com>
@@ -202,7 +202,6 @@ Li Xudong <istonelee@gmail.com>
Michał Gołębiowski-Owczarek <m.goleb@gmail.com>
Renato Oliveira dos Santos <ros3@cin.ufpe.br>
Frederic Junod <frederic.junod@camptocamp.com>
Tom H Fuertes <tomfuertes@gmail.com>
Mitch Foley <mitch@thefoley.net>
ros3cin <ros3@cin.ufpe.br>
Kyle Robinson Young <kyle@dontkry.com>
@@ -250,7 +249,6 @@ Dan Hart <danhart@notonthehighstreet.com>
Nazar Mokrynskyi <nazar@mokrynskyi.com>
Benjamin Tan <demoneaux@gmail.com>
Amit Merchant <bullredeyes@gmail.com>
Jason Bedard <jason+github@jbedard.ca>
Veaceslav Grimalschi <grimalschi@yandex.ru>
Richard McDaniel <rm0026@uah.edu>
Arthur Verschaeve <contact@arthurverschaeve.be>
@@ -273,12 +271,12 @@ Jon Hester <jon.d.hester@gmail.com>
Colin Frick <colin@bash.li>
Winston Howes <winstonhowes@gmail.com>
Alexander O'Mara <me@alexomara.com>
Chris Rebert <github@rebertia.com>
Bastian Buchholz <buchholz.bastian@googlemail.com>
Mu Haibao <mhbseal@163.com>
Calvin Metcalf <calvin.metcalf@gmail.com>
Arthur Stolyar <nekr.fabula@gmail.com>
Gabriel Schulhof <gabriel.schulhof@intel.com>
Chris Rebert <github@rebertia.com>
Gilad Peleg <giladp007@gmail.com>
Julian Alexander Murillo <julian.alexander.murillo@gmail.com>
Kevin Kirsche <Kev.Kirsche+GitHub@gmail.com>
@@ -297,15 +295,14 @@ Christian Grete <webmaster@christiangrete.com>
Tom von Clef <thomas.vonclef@gmail.com>
Liza Ramo <liza.h.ramo@gmail.com>
Joelle Fleurantin <joasqueeniebee@gmail.com>
Steve Mao <maochenyan@gmail.com>
Jon Dufresne <jon.dufresne@gmail.com>
Jae Sung Park <alberto.park@gmail.com>
Josh Soref <apache@soref.com>
Saptak Sengupta <saptak013@gmail.com>
Henry Wong <henryw4k@gmail.com>
Jun Sun <klsforever@gmail.com>
Martijn W. van der Lee <martijn@vanderlee.com>
Devin Wilson <dwilson6.github@gmail.com>
Steve Mao <maochenyan@gmail.com>
Damian Senn <jquery@topaxi.codes>
Zack Hall <zackhall@outlook.com>
Vitaliy Terziev <vitaliyterziev@gmail.com>
@@ -336,6 +333,7 @@ Jordan Beland <jordan.beland@gmail.com>
Henry Zhu <hi@henryzoo.com>
Nilton Cesar <niltoncms@gmail.com>
basil.belokon <basil.belokon@gmail.com>
Saptak Sengupta <saptak013@gmail.com>
Andrey Meshkov <ay.meshkov@gmail.com>
tmybr11 <tomas.perone@gmail.com>
Luis Emilio Velasco Sanchez <emibloque@gmail.com>
@@ -344,14 +342,34 @@ Bert Zhang <enbo@users.noreply.github.com>
Sébastien Règne <regseb@users.noreply.github.com>
wartmanm <3869625+wartmanm@users.noreply.github.com>
Siddharth Dungarwal <sd5869@gmail.com>
abnud1 <ahmad13932013@hotmail.com>
Andrei Fangli <andrei_fangli@outlook.com>
Marja Hölttä <marja.holtta@gmail.com>
abnud1 <ahmad13932013@hotmail.com>
buddh4 <mail@jharrer.de>
Hoang <dangkyokhoang@gmail.com>
Sean Robinson <sean.robinson@scottsdalecc.edu>
Wonseop Kim <wonseop.kim@samsung.com>
Pat O'Callaghan <patocallaghan@gmail.com>
JuanMa Ruiz <ruizjuanma@gmail.com>
Ahmed.S.ElAfifi <ahmed.s.elafifi@gmail.com>
Sean Robinson <sean.robinson@scottsdalecc.edu>
Christian Oliff <christianoliff@pm.me>
Christian Wenz <christian@wenz.org>
Jonathan <vanillajonathan@users.noreply.github.com>
Ed Sanders <ejsanders@gmail.com>
Pierre Grimaud <grimaud.pierre@gmail.com>
Beatriz Rezener <beatrizrezener@users.noreply.github.com>
Necmettin Karakaya <necmettin.karakaya@gmail.com>
Wonhyoung Park <wh05.park@samsung.com>
Dallas Fraser <dallas.fraser.waterloo@gmail.com>
高灰 <www@zeroplace.cn>
fecore1 <89127124+fecore1@users.noreply.github.com>
ygj6 <7699524+ygj6@users.noreply.github.com>
Bruno PIERRE <brunopierre4@yahoo.fr>
Simon Legner <Simon.Legner@gmail.com>
Baoshuo Ren <i@baoshuo.ren>
Anders Kaseorg <andersk@mit.edu>
Alex <aleksandrosansan@gmail.com>
Gabriela Gutierrez <gabigutierrez@google.com>
Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com>
J.Son <x@x-wx.com>
Liam James <liam@minimaximize.com>

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
/*! shiny 1.11.1.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
"use strict";(()=>{document.documentElement.classList.add("autoreload-enabled");var c=window.location.protocol==="https:"?"wss:":"ws:",s=window.location.pathname.replace(/\/?$/,"/")+"autoreload/",i=`${c}//${window.location.host}${s}`,l=document.currentScript?.dataset?.wsUrl||i;async function u(o){let e=new WebSocket(o),n=!1;return new Promise((a,r)=>{e.onopen=()=>{n=!0},e.onerror=t=>{r(t)},e.onclose=()=>{n?a(!1):r(new Error("WebSocket connection failed"))},e.onmessage=function(t){t.data==="autoreload"&&a(!0)}})}async function d(o){return new Promise(e=>setTimeout(e,o))}async function w(){for(;;){try{if(await u(l)){window.location.reload();return}}catch{console.debug("Giving up on autoreload");return}await d(1e3)}}w().catch(o=>{console.error(o)});})();
//# sourceMappingURL=shiny-autoreload.js.map

View File

@@ -2,6 +2,6 @@
"version": 3,
"sources": ["../../../srcts/extras/shiny-autoreload.ts"],
"sourcesContent": ["/* eslint-disable unicorn/filename-case */\n\ndocument.documentElement.classList.add(\"autoreload-enabled\");\n\nconst protocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n// Add trailing slash to path, if necessary, before appending \"autoreload\"\nconst defaultPath =\n window.location.pathname.replace(/\\/?$/, \"/\") + \"autoreload/\";\nconst defaultUrl = `${protocol}//${window.location.host}${defaultPath}`;\n\n// By default, use the defaultUrl. But if there's a data-ws-url attribute on our\n// <script> tag, use that instead.\nconst wsUrl = document.currentScript?.dataset?.wsUrl || defaultUrl;\n\n/**\n * Connects to an autoreload URL and waits for the server to tell us what to do.\n *\n * @param url The ws:// or wss:// URL to connect to.\n * @returns true if the server requests a reload, or false if the connection was\n * successfully established but then closed without the server requesting a\n * reload\n * @throws A nondescript error if the connection fails to be established.\n */\nasync function autoreload(url: string): Promise<boolean> {\n const ws = new WebSocket(url);\n\n let success = false;\n\n return new Promise((resolve, reject) => {\n ws.onopen = () => {\n success = true;\n };\n\n ws.onerror = (err) => {\n reject(err);\n };\n\n ws.onclose = () => {\n if (!success) {\n reject(new Error(\"WebSocket connection failed\"));\n } else {\n resolve(false);\n }\n };\n\n ws.onmessage = function (event) {\n if (event.data === \"autoreload\") {\n resolve(true);\n }\n };\n });\n}\n\nasync function sleep(ms: number) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function initialize() {\n while (true) {\n try {\n if (await autoreload(wsUrl)) {\n window.location.reload();\n return;\n }\n } catch (err) {\n // It's possible for the autoreload() call to throw. If it does, that\n // means we tried but failed to connect to the autoreload socket. This\n // probably means that the entire `shiny run --reload` process was\n // restarted. As of today, the autoreload websocket port number is\n // randomly chosen for each `shiny run --reload` process, so it's\n // impossible for us to recover.\n console.debug(\"Giving up on autoreload\");\n return;\n }\n // If we get here, the connection to the autoreload server was\n // successful but then got broken. Wait for a second, and then\n // try to re-establish the connection.\n await sleep(1000);\n }\n}\n\ninitialize().catch((err) => {\n console.error(err);\n});\n\nexport {};\n"],
"mappings": ";mBAEA,SAAS,gBAAgB,UAAU,IAAI,oBAAoB,EAE3D,IAAMA,EAAW,OAAO,SAAS,WAAa,SAAW,OAAS,MAE5DC,EACJ,OAAO,SAAS,SAAS,QAAQ,OAAQ,GAAG,EAAI,cAC5CC,EAAa,GAAGF,MAAa,OAAO,SAAS,OAAOC,IAIpDE,EAAQ,SAAS,eAAe,SAAS,OAASD,EAWxD,eAAeE,EAAWC,EAA+B,CACvD,IAAMC,EAAK,IAAI,UAAUD,CAAG,EAExBE,EAAU,GAEd,OAAO,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtCH,EAAG,OAAS,IAAM,CAChBC,EAAU,EACZ,EAEAD,EAAG,QAAWI,GAAQ,CACpBD,EAAOC,CAAG,CACZ,EAEAJ,EAAG,QAAU,IAAM,CACZC,EAGHC,EAAQ,EAAK,EAFbC,EAAO,IAAI,MAAM,6BAA6B,CAAC,CAInD,EAEAH,EAAG,UAAY,SAAUK,EAAO,CAC1BA,EAAM,OAAS,cACjBH,EAAQ,EAAI,CAEhB,CACF,CAAC,CACH,CAEA,eAAeI,EAAMC,EAAY,CAC/B,OAAO,IAAI,QAASL,GAAY,WAAWA,EAASK,CAAE,CAAC,CACzD,CAEA,eAAeC,GAAa,CAC1B,OAAa,CACX,GAAI,CACF,GAAI,MAAMV,EAAWD,CAAK,EAAG,CAC3B,OAAO,SAAS,OAAO,EACvB,MACF,CACF,MAAE,CAOA,QAAQ,MAAM,yBAAyB,EACvC,MACF,CAIA,MAAMS,EAAM,GAAI,CAClB,CACF,CAEAE,EAAW,EAAE,MAAOJ,GAAQ,CAC1B,QAAQ,MAAMA,CAAG,CACnB,CAAC",
"mappings": ";mBAEA,SAAS,gBAAgB,UAAU,IAAI,oBAAoB,EAE3D,IAAMA,EAAW,OAAO,SAAS,WAAa,SAAW,OAAS,MAE5DC,EACJ,OAAO,SAAS,SAAS,QAAQ,OAAQ,GAAG,EAAI,cAC5CC,EAAa,GAAGF,CAAQ,KAAK,OAAO,SAAS,IAAI,GAAGC,CAAW,GAI/DE,EAAQ,SAAS,eAAe,SAAS,OAASD,EAWxD,eAAeE,EAAWC,EAA+B,CACvD,IAAMC,EAAK,IAAI,UAAUD,CAAG,EAExBE,EAAU,GAEd,OAAO,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtCH,EAAG,OAAS,IAAM,CAChBC,EAAU,EACZ,EAEAD,EAAG,QAAWI,GAAQ,CACpBD,EAAOC,CAAG,CACZ,EAEAJ,EAAG,QAAU,IAAM,CACZC,EAGHC,EAAQ,EAAK,EAFbC,EAAO,IAAI,MAAM,6BAA6B,CAAC,CAInD,EAEAH,EAAG,UAAY,SAAUK,EAAO,CAC1BA,EAAM,OAAS,cACjBH,EAAQ,EAAI,CAEhB,CACF,CAAC,CACH,CAEA,eAAeI,EAAMC,EAAY,CAC/B,OAAO,IAAI,QAASL,GAAY,WAAWA,EAASK,CAAE,CAAC,CACzD,CAEA,eAAeC,GAAa,CAC1B,OAAa,CACX,GAAI,CACF,GAAI,MAAMV,EAAWD,CAAK,EAAG,CAC3B,OAAO,SAAS,OAAO,EACvB,MACF,CACF,MAAc,CAOZ,QAAQ,MAAM,yBAAyB,EACvC,MACF,CAIA,MAAMS,EAAM,GAAI,CAClB,CACF,CAEAE,EAAW,EAAE,MAAOJ,GAAQ,CAC1B,QAAQ,MAAMA,CAAG,CACnB,CAAC",
"names": ["protocol", "defaultPath", "defaultUrl", "wsUrl", "autoreload", "url", "ws", "success", "resolve", "reject", "err", "event", "sleep", "ms", "initialize"]
}

View File

@@ -1,2 +1,2 @@
/*! shiny 1.11.1.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
#showcase-well{border-radius:0}.shiny-code{background-color:#fff;margin-bottom:0}.shiny-code code{font-family:Menlo,Consolas,Courier New,monospace}.shiny-code-container{margin-top:20px;clear:both}.shiny-code-container h3{display:inline;margin-right:15px}.showcase-header{font-size:16px;font-weight:400}.showcase-code-link{text-align:right;padding:15px}#showcase-app-container{vertical-align:top}#showcase-code-tabs{margin-right:15px}#showcase-code-tabs pre{border:none;line-height:1em}#showcase-code-tabs .nav,#showcase-code-tabs ul{margin-bottom:0}#showcase-code-tabs .tab-content{border-style:solid;border-color:#e5e5e5;border-width:0px 1px 1px 1px;overflow:auto;border-bottom-right-radius:4px;border-bottom-left-radius:4px}#showcase-app-code{width:100%}#showcase-code-position-toggle{float:right}#showcase-sxs-code{padding-top:20px;vertical-align:top}.showcase-code-license{display:block;text-align:right}#showcase-code-content pre{background-color:#fff}

View File

@@ -1,3 +1,3 @@
/*! shiny 1.11.1.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
"use strict";(()=>{var f=400;function c(e,i){let t=0;if(e.nodeType===3){let n=e.nodeValue.replace(/\n/g,"").length;if(n>=i)return{element:e,offset:i};t+=n}else if(e.nodeType===1&&e.firstChild){let n=c(e.firstChild,i);if(n.element!==null)return n;t+=n.offset}return e.nextSibling?c(e.nextSibling,i-t):{element:null,offset:t}}function r(e,i,t){let n=0;for(let s=0;s<e.childNodes.length;s++){let l=e.childNodes[s];if(l.nodeType===3){let o=/\n/g,d;for(;(d=o.exec(l.nodeValue))!==null;)if(n++,n===i)return c(l,d.index+t+1)}else if(l.nodeType===1){let o=r(l,i-n,t);if(o.element!==null)return o;n+=o.offset}}return{element:null,offset:n}}function w(e,i){if(!document.createRange)return;let t=document.getElementById("srcref_"+e);if(!t){t=document.createElement("span"),t.id="srcref_"+e;let n=e,s=document.getElementById(i.replace(/\./g,"_")+"_code");if(!s)return;let l=r(s,n[0],n[4]),o=r(s,n[2],n[5]);if(l.element===null||o.element===null)return;let d=document.createRange();l.element.parentNode.nodeName==="SPAN"&&l.element!==o.element?d.setStartBefore(l.element.parentNode):d.setStart(l.element,l.offset),o.element.parentNode.nodeName==="SPAN"&&l.element!==o.element?d.setEndAfter(o.element.parentNode):d.setEnd(o.element,o.offset),d.surroundContents(t)}$(t).stop(!0,!0).effect("highlight",null,1600)}Shiny&&Shiny.addCustomMessageHandler("showcase-src",function(e){e.srcref&&e.srcfile&&w(e.srcref,e.srcfile)});var a=!1,m=function(e,i){let t=i?f:1,n=e?document.getElementById("showcase-sxs-code"):document.getElementById("showcase-code-inline"),s=e?document.getElementById("showcase-code-inline"):document.getElementById("showcase-sxs-code");if(document.getElementById("showcase-app-metadata")===null){let o=$("#showcase-well");e?o.fadeOut(t):o.fadeIn(t)}$(n).hide(),$(s).fadeOut(t,function(){let o=document.getElementById("showcase-code-tabs");s.removeChild(o),n.appendChild(o),e?h():document.getElementById("showcase-code-content").removeAttribute("style"),$(n).fadeIn(t),e||(document.getElementById("showcase-app-container").removeAttribute("style"),i&&$(document.body).animate({scrollTop:$(n).offset().top}));let d=document.getElementById("readme-md");d!==null&&(d.parentElement.removeChild(d),e?(s.appendChild(d),$(s).fadeIn(t)):document.getElementById("showcase-app-metadata").appendChild(d)),document.getElementById("showcase-code-position-toggle").innerHTML=e?'<i class="fa fa-level-down"></i> show below':'<i class="fa fa-level-up"></i> show with app'}),e&&$(document.body).animate({scrollTop:0},t),a=e,u(e&&i),$(window).trigger("resize")};function u(e){let t=960,n=1,s=document.getElementById("showcase-app-code").offsetWidth;s/2>960?t=s/2:s*.66>960?t=960:(t=s*.66,n=t/960);let l=document.getElementById("showcase-app-container");$(l).animate({width:t+"px",zoom:n*100+"%"},e?f:0)}var g=function(){m(!a,!0)},p=function(){document.body.offsetWidth>1350&&m(!0,!1)};function h(){document.getElementById("showcase-code-content").style.height=$(window).height()+"px"}function y(){let e=document.getElementById("showcase-markdown-content");if(e!==null){let i=e.innerText||e.innerHTML,t=window.Showdown.converter;document.getElementById("readme-md").innerHTML=new t().makeHtml(i)}}$(window).resize(function(){a&&(u(!1),h())});window.toggleCodePosition=g;$(window).on("load",p);$(window).on("load",y);window.hljs&&window.hljs.initHighlightingOnLoad();})();
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
"use strict";(()=>{var m=400;function c(e,s){let t=0;if(e.nodeType===3){let n=e.nodeValue?.replace(/\n/g,"").length??0;if(n>=s)return{element:e,offset:s};t+=n}else if(e.nodeType===1&&e.firstChild){let n=c(e.firstChild,s);if(n.element!==null)return n;t+=n.offset}return e.nextSibling?c(e.nextSibling,s-t):{element:null,offset:t}}function a(e,s,t){let n=0;for(let l=0;l<e.childNodes.length;l++){let i=e.childNodes[l];if(i.nodeType===3){let o=/\n/g,d;for(;(d=o.exec(i.nodeValue))!==null;)if(n++,n===s)return c(i,d.index+t+1)}else if(i.nodeType===1){let o=a(i,s-n,t);if(o.element!==null)return o;n+=o.offset}}return{element:null,offset:n}}function g(e,s){if(!document.createRange)return;let t=document.getElementById("srcref_"+e);if(!t){t=document.createElement("span"),t.id="srcref_"+e;let n=e,l=document.getElementById(s.replace(/\./g,"_")+"_code");if(!l)return;let i=a(l,n[0],n[4]),o=a(l,n[2],n[5]);if(i.element===null||o.element===null)return;let d=document.createRange();i.element.parentNode?.nodeName==="SPAN"&&i.element!==o.element?d.setStartBefore(i.element.parentNode):d.setStart(i.element,i.offset),o.element.parentNode?.nodeName==="SPAN"&&i.element!==o.element?d.setEndAfter(o.element.parentNode):d.setEnd(o.element,o.offset),d.surroundContents(t)}$(t).stop(!0,!0).effect("highlight",null,1600)}window.Shiny&&window.Shiny.addCustomMessageHandler("showcase-src",function(e){e.srcref&&e.srcfile&&g(e.srcref,e.srcfile)});var r=!1,u=function(e,s){let t=s?m:1,n=e?document.getElementById("showcase-sxs-code"):document.getElementById("showcase-code-inline"),l=e?document.getElementById("showcase-code-inline"):document.getElementById("showcase-sxs-code");if(document.getElementById("showcase-app-metadata")===null){let o=$("#showcase-well");e?o.fadeOut(t):o.fadeIn(t)}if(n===null||l===null){console.warn("Could not find the host elements for the code tabs. This is likely a bug in the showcase app.");return}$(n).hide(),$(l).fadeOut(t,function(){let o=document.getElementById("showcase-code-tabs");if(o===null){console.warn("Could not find the code tabs element. This is likely a bug in the showcase app.");return}if(l.removeChild(o),n.appendChild(o),e?p():document.getElementById("showcase-code-content")?.removeAttribute("style"),$(n).fadeIn(t),!e&&(document.getElementById("showcase-app-container")?.removeAttribute("style"),s)){let f=$(n).offset()?.top;f!==void 0&&$(document.body).animate({scrollTop:f})}let d=document.getElementById("readme-md");d!==null&&(d.parentElement?.removeChild(d),e?(l.appendChild(d),$(l).fadeIn(t)):document.getElementById("showcase-app-metadata")?.appendChild(d)),document.getElementById("showcase-code-position-toggle").innerHTML=e?'<i class="fa fa-level-down"></i> show below':'<i class="fa fa-level-up"></i> show with app'}),e&&$(document.body).animate({scrollTop:0},t),r=e,h(e&&s),$(window).trigger("resize")};function h(e){let t=960,n=1,l=document.getElementById("showcase-app-code").offsetWidth;l/2>960?t=l/2:l*.66>960?t=960:(t=l*.66,n=t/960),$("#showcase-app-container").animate({width:t+"px",zoom:n*100+"%"},e?m:0)}var w=function(){u(!r,!0)},y=function(){document.body.offsetWidth>1350&&u(!0,!1)};function p(){document.getElementById("showcase-code-content").style.height=$(window).height()+"px"}function E(){let e=document.getElementById("showcase-markdown-content");if(e!==null){let s=document.getElementById("readme-md");if(s!==null){let t=e.content.cloneNode(!0);s.appendChild(t)}}}$(window).resize(function(){r&&(h(!1),p())});window.toggleCodePosition=w;$(window).on("load",y);$(window).on("load",E);window.hljs&&window.hljs.initHighlightingOnLoad();})();
//# sourceMappingURL=shiny-showcase.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
/*! shiny 1.11.1.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
"use strict";(()=>{var t=eval;window.addEventListener("message",function(a){let e=a.data;e.code&&t(e.code)});})();
//# sourceMappingURL=shiny-testmode.js.map

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -463,10 +463,11 @@ textarea.textarea-autoresize.form-control {
}
}
// Add spacing between icon and label for actionButton()
.action-button:not(.action-link) {
// Add spacing between icon and label for action buttons and links (#4348)
// Using margin instead of padding so the underline doesn't extend into the gap for links
.action-button {
.action-icon + .action-label {
padding-left: 0.5ch;
margin-left: 1ch;
}
}
@@ -480,6 +481,13 @@ textarea.textarea-autoresize.form-control {
cursor: not-allowed;
}
// Selectize's remove_button plugin has a bug that can lead to multiple remove
// buttons being displayed when options get updated. This prevents that from
// happening (see #4274 for reprex)
.shiny-input-select .selectize-input > .item > .remove:not(:first-child) {
display: none;
}
/* Hidden tabPanels */
.nav-hidden {
/* override anything bootstrap sets for `.nav` */

File diff suppressed because one or more lines are too long

View File

@@ -1,34 +0,0 @@
Copyright (c) 2007, John Fraser
<http://www.attacklab.net/>
All rights reserved.
Original Markdown copyright (c) 2004, John Gruber
<http://daringfireball.net/>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name "Markdown" nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
This software is provided by the copyright holders and contributors "as
is" and any express or implied warranties, including, but not limited
to, the implied warranties of merchantability and fitness for a
particular purpose are disclaimed. In no event shall the copyright owner
or contributors be liable for any direct, indirect, incidental, special,
exemplary, or consequential damages (including, but not limited to,
procurement of substitute goods or services; loss of use, data, or
profits; or business interruption) however caused and on any theory of
liability, whether in contract, strict liability, or tort (including
negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
};

View File

@@ -45,8 +45,31 @@ is, a function that quickly returns a promise) and allows even that very
session to immediately unblock and carry on with other user interactions.
}
\section{OpenTelemetry Integration}{
When an \code{ExtendedTask} is created, if OpenTelemetry tracing is enabled for
\code{"reactivity"} (see \code{\link[=withOtelCollect]{withOtelCollect()}}), the \code{ExtendedTask} will record
spans for each invocation of the task. The tracing level at \code{invoke()} time
does not affect whether spans are recorded; only the tracing level when
calling \code{ExtendedTask$new()} matters.
The OTel span will be named based on the label created from the variable the
\code{ExtendedTask} is assigned to. If no label can be determined, the span will
be named \verb{<anonymous>}. Similar to other Shiny OpenTelemetry spans, the span
will also include source reference attributes and session ID attributes.
\if{html}{\out{<div class="sourceCode r">}}\preformatted{withOtelCollect("all", \{
my_task <- ExtendedTask$new(function(...) \{ ... \})
\})
# Span recorded for this invocation: ExtendedTask my_task
my_task$invoke(...)
}\if{html}{\out{</div>}}
}
\examples{
\dontshow{if (rlang::is_interactive() && rlang::is_installed("mirai")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
\dontshow{if (rlang::is_interactive() && rlang::is_installed("mirai")) withAutoprint(\{ # examplesIf}
library(shiny)
library(bslib)
library(mirai)

View File

@@ -83,7 +83,7 @@ instead apply to the entire page, set \code{spinner_selector = 'html'} and
\code{fade_selector = 'html'}.
}
\examples{
\dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
\dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf}
library(bslib)

View File

@@ -56,7 +56,7 @@ operations.
}
\examples{
\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
\dontshow{if (interactive()) withAutoprint(\{ # examplesIf}
library(shiny)
ui <- fixedPage(

View File

@@ -60,6 +60,10 @@ in its \code{DESCRIPTION} file, if any.}
only used for recording or running automated tests. Defaults to the
\code{shiny.testmode} option, or FALSE if the option is not set.}
}
\value{
The value passed to \code{\link[=stopApp]{stopApp()}}, or throws an error if the app was
stopped with an error.
}
\description{
Runs a Shiny application. This function normally does not return; interrupt R
to stop the application (usually by pressing Ctrl+C or Esc).
@@ -109,3 +113,7 @@ if (interactive()) {
runApp(app)
}
}
\seealso{
\code{\link[=startApp]{startApp()}} for non-blocking mode, \code{\link[=stopApp]{stopApp()}} to stop a running
app.
}

View File

@@ -38,7 +38,7 @@ Authors:
Other contributors:
\itemize{
\item Posit Software, PBC (03wc8by49) [copyright holder, funder]
\item Posit Software, PBC (\href{https://ror.org/03wc8by49}{ROR}) [copyright holder, funder]
\item jQuery Foundation (jQuery library and jQuery UI library) [copyright holder]
\item jQuery contributors (jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt) [contributor, copyright holder]
\item jQuery UI contributors (jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt) [contributor, copyright holder]
@@ -59,8 +59,6 @@ Other contributors:
\item Denis Ineshin (ion.rangeSlider library) [contributor, copyright holder]
\item Sami Samhuri (Javascript strftime library) [contributor, copyright holder]
\item SpryMedia Limited (DataTables library) [contributor, copyright holder]
\item John Fraser (showdown.js library) [contributor, copyright holder]
\item John Gruber (showdown.js library) [contributor, copyright holder]
\item Ivan Sagalaev (highlight.js library) [contributor, copyright holder]
\item R Core Team (tar implementation from R) [contributor, copyright holder]
}

View File

@@ -130,6 +130,32 @@ ragg package. See \code{\link[=plotPNG]{plotPNG()}} for more information.}
Cairo package. See \code{\link[=plotPNG]{plotPNG()}} for more information.}
\item{shiny.devmode (defaults to \code{NULL})}{Option to enable Shiny Developer Mode. When set,
different default \code{getOption(key)} values will be returned. See \code{\link[=devmode]{devmode()}} for more details.}
\item{shiny.otel.collect (defaults to \code{Sys.getenv("SHINY_OTEL_COLLECT", "all")})}{Determines how Shiny will interact with OpenTelemetry.
Supported values:
\itemize{
\item \code{"none"} - No Shiny OpenTelemetry tracing.
\item \code{"session"} - Adds session start/end spans.
\item \code{"reactive_update"} - Spans for any synchronous/asynchronous reactive
update. (Includes \code{"session"} features).
\item \code{"reactivity"} - Spans for all reactive expressions and logs for setting
reactive vals and values. (Includes \code{"reactive_update"} features). This
option must be set when creating any reactive objects that should record
OpenTelemetry spans / logs. See \code{\link[=withOtelCollect]{withOtelCollect()}} and
\code{\link[=localOtelCollect]{localOtelCollect()}} for ways to set this option locally when creating
your reactive expressions.
\item \code{"all"} - All Shiny OpenTelemetry tracing. Currently equivalent to
\code{"reactivity"}.
}
This option is useful for debugging and profiling while in production. This
option will only be useful if the \code{otelsdk} package is installed and
\code{otel::is_tracing_enabled()} returns \code{TRUE}. Please have any OpenTelemetry
environment variables set before loading any relevant R packages.
To set this option locally within a specific part of your Shiny
application, see \code{\link[=withOtelCollect]{withOtelCollect()}} and \code{\link[=localOtelCollect]{localOtelCollect()}}.}
\item{shiny.otel.sanitize.errors (defaults to \code{TRUE})}{If \code{TRUE}, fatal and unhandled errors will be sanitized before being sent to the OpenTelemetry backend. The default value of \code{TRUE} is set to avoid potentially sending sensitive information to the OpenTelemetry backend. If you want the full error message and stack trace to be sent to the OpenTelemetry backend, set this option to \code{FALSE} or use \code{safeError(e)}.}
}
}

91
man/startApp.Rd Normal file
View File

@@ -0,0 +1,91 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/runapp.R
\name{startApp}
\alias{startApp}
\title{Start Shiny Application (Non-Blocking)}
\usage{
startApp(
appDir = getwd(),
port = getOption("shiny.port"),
launch.browser = getOption("shiny.launch.browser", interactive()),
host = getOption("shiny.host", "127.0.0.1"),
workerId = "",
quiet = FALSE,
display.mode = c("auto", "normal", "showcase"),
test.mode = getOption("shiny.testmode", FALSE)
)
}
\arguments{
\item{appDir}{The application to run. Should be one of the following:
\itemize{
\item A directory containing \code{server.R}, plus, either \code{ui.R} or
a \code{www} directory that contains the file \code{index.html}.
\item A directory containing \code{app.R}.
\item An \code{.R} file containing a Shiny application, ending with an
expression that produces a Shiny app object.
\item A list with \code{ui} and \code{server} components.
\item A Shiny app object created by \code{\link[=shinyApp]{shinyApp()}}.
}}
\item{port}{The TCP port that the application should listen on. If the
\code{port} is not specified, and the \code{shiny.port} option is set (with
\code{options(shiny.port = XX)}), then that port will be used. Otherwise,
use a random port between 3000:8000, excluding ports that are blocked
by Google Chrome for being considered unsafe: 3659, 4045, 5060,
5061, 6000, 6566, 6665:6669 and 6697. Up to twenty random
ports will be tried.}
\item{launch.browser}{If true, the system's default web browser will be
launched automatically after the app is started. Defaults to true in
interactive sessions only. The value of this parameter can also be a
function to call with the application's URL.}
\item{host}{The IPv4 address that the application should listen on. Defaults
to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not. See
Details.}
\item{workerId}{Can generally be ignored. Exists to help some editions of
Shiny Server Pro route requests to the correct process.}
\item{quiet}{Should Shiny status messages be shown? Defaults to FALSE.}
\item{display.mode}{The mode in which to display the application. If set to
the value \code{"showcase"}, shows application code and metadata from a
\code{DESCRIPTION} file in the application directory alongside the
application. If set to \code{"normal"}, displays the application normally.
Defaults to \code{"auto"}, which displays the application in the mode given
in its \code{DESCRIPTION} file, if any.}
\item{test.mode}{Should the application be launched in test mode? This is
only used for recording or running automated tests. Defaults to the
\code{shiny.testmode} option, or FALSE if the option is not set.}
}
\value{
A \code{ShinyAppHandle} object with methods \code{stop()}, \code{status()},
\code{url()}, and \code{result()}. The \code{status()} method returns \code{"running"},
\code{"success"}, or \code{"error"}. The \code{result()} method throws an error if called
while running, or re-throws the error if the app stopped with an error.
}
\description{
Starts a Shiny application in non-blocking mode, returning a
\code{ShinyAppHandle} immediately while the app runs in the background.
The \code{later} event loop services the app, so the R console remains
available for interaction.
}
\examples{
\dontrun{
# Start app in the background
handle <- startApp("myapp")
# Check status
handle$status()
handle$url()
# Stop the app
handle$stop()
}
}
\seealso{
\code{\link[=runApp]{runApp()}} for blocking mode, \code{\link[=stopApp]{stopApp()}} to stop a running app.
}

View File

@@ -12,5 +12,7 @@ stopApp(returnValue = invisible())
}
\description{
Stops the currently running Shiny app, returning control to the caller of
\code{\link[=runApp]{runApp()}}.
\code{\link[=runApp]{runApp()}}. Despite the similar names, \code{stopApp()} is not the
counterpart of \code{\link[=startApp]{startApp()}} — it is the counterpart of \code{\link[=runApp]{runApp()}},
controlling its return value via \code{returnValue}.
}

View File

@@ -35,7 +35,7 @@ automatically disabled when spinner(s) are active. When both \code{spinners} and
graying out of recalculating outputs).
}
\examples{
\dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
\dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf}
library(bslib)

107
man/withOtelCollect.Rd Normal file
View File

@@ -0,0 +1,107 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/otel-with.R
\name{withOtelCollect}
\alias{withOtelCollect}
\alias{localOtelCollect}
\title{Temporarily set OpenTelemetry (OTel) collection level}
\usage{
withOtelCollect(collect, expr)
localOtelCollect(collect, envir = parent.frame())
}
\arguments{
\item{collect}{Character string specifying the OpenTelemetry collection level.
Must be one of the following:
\if{html}{\out{<div class="sourceCode">}}\preformatted{* `"none"` - No telemetry data collected
* `"reactivity"` - Collect reactive execution spans (includes session and
reactive update events)
* `"all"` - All available telemetry (currently equivalent to `"reactivity"`)
}\if{html}{\out{</div>}}}
\item{expr}{Expression to evaluate with the specified collection level
(for \code{withOtelCollect()}).}
\item{envir}{Environment where the collection level should be set
(for \code{localOtelCollect()}). Defaults to the parent frame.}
}
\value{
\itemize{
\item \code{withOtelCollect()} returns the value of \code{expr}.
\item \code{localOtelCollect()} is called for its side effect and returns the previous
\code{collect} value invisibly.
}
}
\description{
Control Shiny's OTel collection level for particular reactive expression(s).
\code{withOtelCollect()} sets the OpenTelemetry collection level for
the duration of evaluating \code{expr}. \code{localOtelCollect()} sets the collection
level for the remainder of the current function scope.
}
\details{
Note that \code{"session"} and \code{"reactive_update"} levels are not permitted as
these are runtime-specific levels that should only be set permanently via
\code{options(shiny.otel.collect = ...)} or the \code{SHINY_OTEL_COLLECT} environment
variable, not temporarily during reactive expression creation.
}
\section{Best practice}{
Best practice is to set the collection level for code that \emph{creates} reactive
expressions, not code that \emph{runs} them. For instance:
\if{html}{\out{<div class="sourceCode r">}}\preformatted{# Disable telemetry for a reactive expression
withOtelCollect("none", \{
my_reactive <- reactive(\{ ... \})
\})
# Disable telemetry for a render function
withOtelCollect("none", \{
output$my_plot <- renderPlot(\{ ... \})
\})
#' # Disable telemetry for an observer
withOtelCollect("none", \{
observe(\{ ... \}))
\})
# Disable telemetry for an entire module
withOtelCollect("none", \{
my_result <- my_module("my_id")
\})
# Use `my_result` as normal here
}\if{html}{\out{</div>}}
NOTE: It's not recommended to pipe existing reactive objects into
\code{withOtelCollect()} since they won't inherit their intended OTel settings,
leading to confusion.
}
\examples{
\dontrun{
# Temporarily disable telemetry collection
withOtelCollect("none", {
# Code here won't generate telemetry
reactive({ input$x + 1 })
})
# Collect reactivity telemetry but not other events
withOtelCollect("reactivity", {
# Reactive execution will be traced
observe({ print(input$x) })
})
# Use local variant in a function
my_function <- function() {
localOtelCollect("none")
# Rest of function executes without telemetry
reactive({ input$y * 2 })
}
}
}
\seealso{
See the \code{shiny.otel.collect} option within \code{\link{shinyOptions}}. Setting
this value will globally control OpenTelemetry collection levels.
}

6328
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,94 +1,76 @@
{
"private": true,
"homepage": "https://shiny.rstudio.com",
"repository": "github:rstudio/shiny",
"name": "@types/rstudio-shiny",
"version": "1.11.1-alpha.9000",
"license": "GPL-3.0-only",
"homepage": "https://shiny.posit.co",
"repository": {
"type": "git",
"url": "git+https://github.com/rstudio/shiny.git"
},
"name": "@posit/shiny",
"version": "1.13.0-alpha.9000",
"license": "MIT",
"main": "",
"browser": "",
"types": "srcts/types/extras/globalShiny.d.ts",
"files": [
"DESCRIPTION",
"LICENSE",
"NEWS.md",
"srcts/types/**/*.d.ts"
],
"engines": {
"node": ">= 14",
"yarn": ">= 1.22"
"node": ">= 18",
"npm": ">= 10"
},
"dependencies": {
"@types/bootstrap": "3.4.0",
"@types/bootstrap-datepicker": "0.0.14",
"@types/datatables.net": "^1.10.19",
"@types/bootstrap": "5.2.x",
"@types/bootstrap-datepicker": "1.10.0",
"@types/datatables.net": "1.10.x",
"@types/ion-rangeslider": "2.3.0",
"@types/jquery": "3.5.32",
"@types/selectize": "0.12.34",
"lit": "^3.0.0"
"@types/selectize": "0.12.x",
"lit": "3.2.x"
},
"devDependencies": {
"@deanc/esbuild-plugin-postcss": "^1.0.2",
"@selectize/selectize": "https://github.com/selectize/selectize.js.git#e3f2e0b4aa251375bc21b5fcd8ca7d374a921f08",
"@testing-library/dom": "^7.31.0",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/user-event": "^13.1.9",
"@types/highlightjs": "^9.12.1",
"@types/jest": "^26.0.23",
"@types/highlightjs": "9.x",
"@types/jqueryui": "1.12.24",
"@types/lodash": "^4.14.170",
"@types/node": "^18.14.2",
"@types/showdown": "^1.9.3",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"autoprefixer": "^10.2.6",
"browserslist": "^4.19.1",
"caniuse-lite": "^1.0.30001312",
"core-js": "^3.13.0",
"esbuild": "^0.15.10",
"esbuild-plugin-globals": "^0.1.1",
"esbuild-plugin-sass": "^1.0.1",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.0.4",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^43.0.2",
"fs-readdir-recursive": "^1.1.0",
"jest": "^26.6.3",
"@types/lodash": "4.x",
"@types/node": "18.x",
"@typescript-eslint/eslint-plugin": "8.x",
"@typescript-eslint/parser": "8.x",
"autoprefixer": "10.x",
"bootstrap-datepicker": "1.10.0",
"esbuild": ">=0.20.0",
"esbuild-plugin-globals": "0.1.x",
"esbuild-sass-plugin": "3.x",
"eslint": "9.x",
"eslint-config-prettier": "10.x",
"eslint-plugin-prettier": "5.x",
"eslint-plugin-unicorn": "56.x",
"fs-readdir-recursive": "1.x",
"jquery": "3.7.1",
"lodash": "^4.17.21",
"madge": "^4.0.2",
"node-gyp": "^8.1.0",
"phantomjs-prebuilt": "^2.1.16",
"postcss": "^8.3.5",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.2.4",
"readcontrol": "^1.0.0",
"replace": "^1.2.1",
"ts-jest": "^26",
"ts-node": "^10.9.1",
"type-coverage": "^2.22.0",
"typescript": "^4.8.4",
"util-inspect": "https://github.com/deecewan/browser-util-inspect#c0b4350df4378ffd743e8c36dd3898ce3992823e"
"lodash": "4.x",
"postcss": "8.x",
"postcss-preset-env": "10.2.x",
"prettier": "3.x",
"prettier-plugin-organize-imports": "4.x",
"readcontrol": "1.x",
"tsx": "4.x",
"type-coverage": "2.29.x",
"typescript": "5.8.x"
},
"scripts": {
"prepare": "node --eval \"1+1; // help yarn users not install the whole repo; https://github.com/yarnpkg/yarn/issues/2822#issuecomment-847360267\"",
"watch": "yarn run build_shiny --watch",
"build": "yarn run build_shiny && yarn run bundle_extras && yarn run bundle_external_libs",
"build_shiny": "yarn run checks && yarn run bundle_shiny",
"bundle_shiny": "ts-node srcts/build/shiny.ts",
"bundle_external_libs": "ts-node srcts/build/external_libs.ts",
"bundle_extras": "ts-node srcts/build/extras.ts",
"test": "jest --coverage",
"test_phantom": "echo '\n\t!! Must manually stop phantomjs test !!\n\n' && yarn bundle_shiny && phantomjs --debug=yes ../inst/www/shared/shiny.js",
"checks": "yarn run lint && yarn run build_types && yarn run coverage && yarn run circular",
"lint": "node --eval \"console.log('linting code...')\" && eslint --fix --ext .ts srcts/src",
"watch": "npm run bundle_shiny -- --watch",
"build": "npm run build_shiny && npm run bundle_extras && npm run bundle_external_libs",
"build_shiny": "npm run checks && npm run bundle_shiny",
"bundle_shiny": "tsx srcts/build/shiny.ts",
"bundle_external_libs": "tsx srcts/build/external_libs.ts",
"bundle_extras": "tsx srcts/build/extras.ts",
"checks": "npm run lint && npm run build_types && npm run coverage && npm run circular",
"lint": "node --eval \"console.log('linting code...')\" && eslint 'srcts/src/**/*.ts' --fix",
"build_types": "tsc -p tsconfig.json",
"coverage_detailed": "yarn type-check --detail",
"coverage_detailed": "npx --yes type-check --detail",
"coverage": "type-coverage -p tsconfig.json --at-least 90",
"circular": "madge --circular --extensions ts srcts/src",
"circular_image": "madge --circular --extensions ts --image madge.svg srcts/src"
"circular": "npx --yes dpdm --transform ./srcts/src/index.ts",
"prepack": "cp README.md README-orig.md && cp README-npm.md README.md",
"postpack": "test -f README-orig.md && cp README-orig.md README.md && rm README-orig.md"
},
"prettier": {
"plugins": [

View File

@@ -1,28 +1,45 @@
# Revdeps
## Failed to check (22)
## Failed to check (38)
|package |version |error |warning |note |
|:--------------------|:-------|:-----|:-------|:----|
|bigPint |? | | | |
|bioCancer |? | | | |
|EBImage |? | | | |
|FAfA |0.3 |1 | | |
|ADAMgui |? | | | |
|AssumpSure |? | | | |
|boinet |1.5.0 |1 | | |
|brms |? | | | |
|cheem |? | | | |
|ctsem |3.10.4 |1 | |1 |
|detourr |? | | | |
|EMMAgeo |? | | | |
|FactEff |? | | | |
|FAfA |? | | | |
|fio |0.1.6 |1 | | |
|GeneNetworkBuilder |? | | | |
|gradientPickerD3 |? | | | |
|InterCellar |? | | | |
|LACE |? | | | |
|lavaan.shiny |1.2 |1 | | |
|LDABiplots |? | | | |
|LDAShiny |? | | | |
|fitteR |? | | | |
|FossilSimShiny |? | | | |
|GDINA |? | | | |
|ggsem |? | | | |
|grandR |? | | | |
|GSVA |? | | | |
|hbsaems |? | | | |
|langevitour |? | | | |
|lavaan.shiny |? | | | |
|lcsm |? | | | |
|linkspotter |? | | | |
|loon.shiny |? | | | |
|MatrixQCvis |? | | | |
|metricsgraphics |? | | | |
|modchart |? | | | |
|omicsViewer |? | | | |
|RSP |0.4 |1 | | |
|rstanarm |2.32.1 |1 | | |
|sphereML |0.1.1 |1 | | |
|MOsemiind |0.1.0 |1 | | |
|MVN |? | | | |
|pandemonium |? | | | |
|polarisR |? | | | |
|Prostar |? | | | |
|RCTrep |1.2.0 |1 | | |
|recmap |? | | | |
|rstanarm |2.32.2 |1 | | |
|semdrw |? | | | |
|shotGroups |? | | | |
|sphereML |? | | | |
|spinifex |? | | | |
|StatTeacherAssistant |? | | | |
|TestAnaAPP |1.1.2 |1 | | |
|SurprisalAnalysis |? | | | |
|TestAnaAPP |? | | | |

View File

@@ -1,19 +1,42 @@
## revdepcheck results
We checked 1344 reverse dependencies (1330 from CRAN + 14 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package.
We checked 1383 reverse dependencies (1376 from CRAN + 7 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package.
* We saw 0 new problems
* We failed to check 8 packages
* We failed to check 31 packages
Issues with CRAN packages are summarised below.
### Failed to check
* FAfA (NA)
* fio (NA)
* lavaan.shiny (NA)
* loon.shiny (NA)
* RSP (NA)
* rstanarm (NA)
* sphereML (NA)
* TestAnaAPP (NA)
* AssumpSure (NA)
* boinet (NA)
* brms (NA)
* cheem (NA)
* ctsem (NA)
* detourr (NA)
* FAfA (NA)
* fio (NA)
* fitteR (NA)
* FossilSimShiny (NA)
* GDINA (NA)
* ggsem (NA)
* grandR (NA)
* hbsaems (NA)
* langevitour (NA)
* lavaan.shiny (NA)
* lcsm (NA)
* linkspotter (NA)
* loon.shiny (NA)
* MOsemiind (NA)
* MVN (NA)
* pandemonium (NA)
* polarisR (NA)
* RCTrep (NA)
* rstanarm (NA)
* semdrw (NA)
* shotGroups (NA)
* sphereML (NA)
* spinifex (NA)
* SurprisalAnalysis (NA)
* TestAnaAPP (NA)

Some files were not shown because too many files have changed in this diff Show More