## ----include = FALSE---------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ## ----prerequisites------------------------------------------------------------ # # 1. Shiny >= 1.12.0 (when OTEL support was added) # packageVersion("shiny") # Should be >= 1.12.0 # # # 2. OpenTelemetry packages # install.packages("otel") # install.packages("otelsdk") # # # 3. bidux with OTEL support # install.packages("bidux") # or development version from GitHub ## ----basic_setup-------------------------------------------------------------- # library(shiny) # # # Enable OTEL collection for all events # options(shiny.otel.collect = "all") # # # Or use environment variable (set before starting R) # # Sys.setenv(SHINY_OTEL_COLLECT = "all") # # # Your Shiny app code # ui <- fluidPage( # titlePanel("Sales Dashboard"), # sidebarLayout( # sidebarPanel( # selectInput("region", "Region:", # choices = c("North", "South", "East", "West")), # dateRangeInput("date_range", "Date Range:") # ), # mainPanel( # plotOutput("sales_plot") # ) # ) # ) # # server <- function(input, output, session) { # output$sales_plot <- renderPlot({ # # Your plotting logic # }) # } # # shinyApp(ui, server) ## ----collection_levels-------------------------------------------------------- # # For development and UX analysis - collect everything # options(shiny.otel.collect = "all") # # # For production with moderate overhead # options(shiny.otel.collect = "reactive_update") # # # Minimal production tracking # options(shiny.otel.collect = "session") ## ----file_export-------------------------------------------------------------- # \dontrun{ # library(otel) # library(otelsdk) # # # Configure OTLP JSON file exporter # Sys.setenv( # OTEL_TRACES_EXPORTER = "otlp", # OTEL_EXPORTER_OTLP_PROTOCOL = "http/json", # OTEL_EXPORTER_OTLP_ENDPOINT = "/path/to/otel_spans.json" # ) # # # Enable collection # options(shiny.otel.collect = "all") # # # Run your Shiny app # # Spans will be exported to otel_spans.json # } ## ----json_export, echo=FALSE-------------------------------------------------- # NA ## ----sqlite_export------------------------------------------------------------ # \dontrun{ # library(otel) # library(otelsdk) # # # Configure SQLite exporter (custom implementation) # # Note: Requires additional setup - see otel package documentation # Sys.setenv( # OTEL_TRACES_EXPORTER = "sqlite", # OTEL_EXPORTER_SQLITE_PATH = "/path/to/otel_spans.sqlite" # ) # # options(shiny.otel.collect = "all") # } ## ----live_export-------------------------------------------------------------- # \dontrun{ # library(otel) # library(otelsdk) # # # Configure OTLP HTTP exporter to send to collector # Sys.setenv( # OTEL_TRACES_EXPORTER = "otlp", # OTEL_EXPORTER_OTLP_PROTOCOL = "http/protobuf", # OTEL_EXPORTER_OTLP_ENDPOINT = "https://collector.example.com:4318", # OTEL_EXPORTER_OTLP_HEADERS = "Authorization=Bearer YOUR_TOKEN", # OTEL_RESOURCE_ATTRIBUTES = "service.name=my-shiny-app,environment=production" # ) # # options(shiny.otel.collect = "all") # } ## ----env_vars----------------------------------------------------------------- # # Core configuration # SHINY_OTEL_COLLECT = "all" # Collection level # OTEL_TRACES_EXPORTER = "otlp" # Exporter type # # # OTLP configuration # OTEL_EXPORTER_OTLP_ENDPOINT = "https://collector:4318" # Collector URL # OTEL_EXPORTER_OTLP_PROTOCOL = "http/json" # Protocol format # OTEL_EXPORTER_OTLP_HEADERS = "Authorization=Bearer token" # Auth headers # # # Resource attributes (metadata) # OTEL_RESOURCE_ATTRIBUTES = "service.name=my-app,environment=prod,version=1.0" # # # Sampling (control data volume) # OTEL_TRACES_SAMPLER = "traceidratio" # Sampler type # OTEL_TRACES_SAMPLER_ARG = "0.1" # Sample 10% of traces ## ----bidux_workflow----------------------------------------------------------- # library(bidux) # library(dplyr) # # # Works just like shiny.telemetry! # issues <- bid_telemetry("otel_spans.json") # # # Same friction detection # critical_issues <- issues |> # filter(severity == "critical") |> # arrange(desc(impact_rate)) # # # Same BID pipeline # interpret <- bid_interpret( # central_question = "How to improve user experience based on OTEL data?" # ) # # notices <- bid_notices( # issues = critical_issues, # previous_stage = interpret, # max_issues = 3 # ) # # # Extract telemetry flags # flags <- bid_flags(issues) # flags$has_critical_issues ## ----auto_detect-------------------------------------------------------------- # # Automatically detects shiny.telemetry format # issues_st <- bid_telemetry("telemetry.sqlite") # # # Automatically detects OTLP JSON format # issues_otel <- bid_telemetry("otel_spans.json") # # # Automatically detects OTEL SQLite format # issues_otel_db <- bid_telemetry("otel_spans.sqlite") # # # Same analysis, same results, regardless of source! ## ----otel_duration------------------------------------------------------------ # # Duration calculated from span timestamps # # duration_ms = (endTimeUnixNano - startTimeUnixNano) / 1e6 # # # Analyze OTEL data # issues <- bid_telemetry("otel_spans.json") # # # OTEL data provides performance context # issues |> # filter(issue_type == "delayed_interaction") |> # select(problem, evidence) # #> Problem: Users take a long time before making their first interaction # #> Evidence: Median time to first input is 47 seconds ## ----complete_example--------------------------------------------------------- # \dontrun{ # # ============================================ # # STEP 1: Configure OTEL in your Shiny app # # ============================================ # # library(shiny) # library(otel) # library(otelsdk) # # # Enable OTEL with file export # Sys.setenv( # OTEL_TRACES_EXPORTER = "otlp", # OTEL_EXPORTER_OTLP_ENDPOINT = "/tmp/shiny_otel.json" # ) # options(shiny.otel.collect = "all") # # # Your Shiny app # ui <- fluidPage( # titlePanel("Sales Dashboard"), # sidebarLayout( # sidebarPanel( # selectInput("region", "Region:", # choices = c("North", "South", "East", "West")), # selectInput("product", "Product:", # choices = c("A", "B", "C")), # dateRangeInput("dates", "Date Range:") # ), # mainPanel( # tabsetPanel( # tabPanel("Overview", plotOutput("overview")), # tabPanel("Details", tableOutput("details")), # tabPanel("Settings", uiOutput("settings")) # ) # ) # ) # ) # # server <- function(input, output, session) { # output$overview <- renderPlot({ # # Plotting logic # }) # # output$details <- renderTable({ # # Table logic # }) # # output$settings <- renderUI({ # # Settings UI # }) # } # # # Run app and collect data # shinyApp(ui, server) # # # ============================================ # # STEP 2: Analyze OTEL data with bidux # # ============================================ # # library(bidux) # library(dplyr) # # # Analyze collected OTEL spans # issues <- bid_telemetry( # "/tmp/shiny_otel.json", # thresholds = bid_telemetry_presets("moderate") # ) # # # Review identified issues # print(issues) # #> # BID Telemetry Issues Summary # #> Found 5 issues from 342 sessions # #> # #> Critical: 1 issue # #> High: 2 issues # #> Medium: 2 issues # # # Filter to critical issues # critical <- issues |> # filter(severity == "critical") # # print(critical) # #> Issue: unused_input_product # #> Problem: Users are not interacting with the 'product' input control # #> Evidence: Only 12 out of 342 sessions (3.5%) interacted with 'product' # #> Impact: 96.5% of sessions affected # # # ============================================ # # STEP 3: Apply BID framework # # ============================================ # # # Start BID workflow with OTEL insights # interpret_result <- bid_interpret( # central_question = "Why aren't users engaging with the product filter?", # data_story = new_data_story( # hook = "96.5% of users never use the product filter", # context = "OTEL data from 342 sessions over 2 weeks", # tension = "Filter may be unnecessary or poorly positioned", # resolution = "Simplify interface or improve filter discoverability" # ) # ) # # # Convert OTEL issue to Notice # notice_result <- bid_notices( # issues = critical, # previous_stage = interpret_result # )[[1]] # # # Continue through BID stages # anticipate_result <- bid_anticipate( # previous_stage = notice_result, # bias_mitigations = list( # choice_overload = "Reduce number of visible filters", # default_effect = "Set smart defaults based on common patterns" # ) # ) # # # Use OTEL flags to inform structure # flags <- bid_flags(issues) # structure_result <- bid_structure( # previous_stage = anticipate_result, # telemetry_flags = flags # ) # # # Validate # validate_result <- bid_validate( # previous_stage = structure_result, # summary_panel = "Simplified dashboard with progressive disclosure", # next_steps = c( # "Remove or hide unused product filter", # "Re-run OTEL analysis to verify improvement", # "Monitor user engagement metrics" # ) # ) # # # Generate report # bid_report(validate_result, format = "html") # } ## ----dual_tracking------------------------------------------------------------ # \dontrun{ # library(shiny) # library(shiny.telemetry) # library(otel) # # # Enable both systems # telemetry <- Telemetry$new() # options(shiny.otel.collect = "all") # # ui <- fluidPage( # use_telemetry(), # shiny.telemetry # # Your UI # ) # # server <- function(input, output, session) { # telemetry$start_session() # shiny.telemetry # # Your server logic # } # # # Analyze both sources # issues_st <- bid_telemetry("telemetry.sqlite") # issues_otel <- bid_telemetry("otel_spans.json") # # # Compare results # nrow(issues_st) # nrow(issues_otel) # } ## ----migration_phase1--------------------------------------------------------- # \dontrun{ # # Run both systems to compare # options(shiny.otel.collect = "all") # # Keep existing shiny.telemetry code # # # Compare results weekly # issues_st <- bid_telemetry("telemetry.sqlite") # issues_otel <- bid_telemetry("otel_spans.json") # # # Verify OTEL captures same issues # } ## ----migration_phase2--------------------------------------------------------- # \dontrun{ # # Switch to OTEL as primary # options(shiny.otel.collect = "all") # # Keep shiny.telemetry as backup # # # Use OTEL data for analysis # issues <- bid_telemetry("otel_spans.json") # } ## ----migration_phase3--------------------------------------------------------- # \dontrun{ # # Remove shiny.telemetry code # # library(shiny.telemetry) - remove # # use_telemetry() - remove # # telemetry$start_session() - remove # # # OTEL only # options(shiny.otel.collect = "all") # } ## ----troubleshoot_1----------------------------------------------------------- # # Solution: Install OpenTelemetry packages # install.packages("otel") # install.packages("otelsdk") ## ----troubleshoot_2----------------------------------------------------------- # # Check if OTEL is enabled # getOption("shiny.otel.collect") # #> Should return "all" or another collection level # # # Verify otel is tracing # library(otel) # otel::is_tracing_enabled() # #> Should return TRUE # # # Enable OTEL if disabled # options(shiny.otel.collect = "all") ## ----troubleshoot_3----------------------------------------------------------- # # Verify file structure # jsonlite::fromJSON("otel_spans.json", simplifyVector = FALSE) |> # str(max.level = 2) # # # Should contain spans with startTimeUnixNano, endTimeUnixNano, etc. # # If not, check OTLP exporter configuration ## ----troubleshoot_4----------------------------------------------------------- # # Check exporter endpoint # Sys.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") # # # Verify file path is writable # file.access("otel_spans.json", mode = 2) # #> Should return 0 (success) # # # Check Shiny app had user interactions # # Spans only created when actions occur ## ----troubleshoot_5----------------------------------------------------------- # # Use sampling to reduce volume # Sys.setenv( # OTEL_TRACES_SAMPLER = "traceidratio", # OTEL_TRACES_SAMPLER_ARG = "0.1" # Sample 10% of traces # ) # # # Or reduce collection level # options(shiny.otel.collect = "reactive_update") # Less than "all" ## ----troubleshoot_6----------------------------------------------------------- # # Explicitly specify format # issues <- bid_telemetry("otel_spans.json", format = "otlp_json") # # # Or for OTEL SQLite # issues <- bid_telemetry("otel_spans.sqlite", format = "otel_sqlite") ## ----custom_attributes-------------------------------------------------------- # \dontrun{ # library(otel) # # # Add custom attributes to current span # otel::add_span_attribute("user_role", "analyst") # otel::add_span_attribute("dashboard_version", "2.1.0") # # # These attributes are preserved in OTLP exports # # and available for custom analysis # } ## ----filter_attributes-------------------------------------------------------- # \dontrun{ # # Analyze OTEL data # issues <- bid_telemetry("otel_spans.json") # # # Access raw span data for custom filtering # # (Advanced: requires understanding OTLP structure) # raw_spans <- jsonlite::fromJSON("otel_spans.json") # # # Filter spans by custom attributes before analysis # # Then re-analyze with bidux # } ## ----bp1---------------------------------------------------------------------- # options(shiny.otel.collect = "all") ## ----bp2---------------------------------------------------------------------- # Sys.setenv(OTEL_TRACES_SAMPLER_ARG = "0.1") # 10% sampling ## ----bp3---------------------------------------------------------------------- # \dontrun{ # # Implement log rotation in your deployment # # Example: daily rotation with retention # file_pattern <- paste0("otel_spans_", Sys.Date(), ".json") # } ## ----bp4---------------------------------------------------------------------- # \dontrun{ # file.size("otel_spans.json") / 1024 / 1024 # Size in MB # } ## ----bp5---------------------------------------------------------------------- # # Schedule regular UX reviews # issues <- bid_telemetry("otel_spans.json") # if (any(issues$severity == "critical")) { # # Alert team # } ## ----bp6---------------------------------------------------------------------- # # Use OTEL to identify friction points # # Then interview users to understand root causes