Integrating shiny.telemetry with bidux

Overview

The {bidux} package provides powerful tools to analyze real user behavior data from {shiny.telemetry} and automatically identify UX friction points. Think of it as A/B testing for UX design—you can measure the impact of design changes on user behavior with the same rigor you apply to data analysis.

The power of telemetry + BID: Instead of guessing what users struggle with, you can systematically identify friction points from real usage data, then apply behavioral science principles to fix them.

This creates a powerful feedback loop where actual usage patterns drive design improvements through the BID framework.

New in 0.3.1: Enhanced telemetry workflow with hybrid objects and streamlined API for better integration with tidy data workflows.

Modern Telemetry API (0.3.1+)

The new bid_telemetry() function provides a clean, tidy approach to telemetry analysis:

library(bidux)
library(dplyr)

# Modern approach: get tidy issues tibble
issues <- bid_telemetry("telemetry.sqlite")
print(issues)  # Shows organized issue summary

# Triage critical issues
critical <- issues |>
  filter(severity == "critical") |>
  slice_head(n = 3)

# Convert to Notice stages with bridge functions
notices <- bid_notices(
  issues = critical,
  previous_stage = interpret_result
)

# Extract telemetry flags for layout optimization
flags <- bid_flags(issues)
structure_result <- bid_structure(
  previous_stage = anticipate_result,
  telemetry_flags = flags
)

Legacy Compatibility

The bid_ingest_telemetry() function now returns a hybrid object that maintains full backward compatibility:

# Legacy approach still works exactly as before
legacy_notices <- bid_ingest_telemetry("telemetry.sqlite")
length(legacy_notices)  # Behaves like list of Notice stages
legacy_notices[[1]]     # Access individual Notice objects

# But now also provides enhanced functionality
as_tibble(legacy_notices)  # Get tidy issues view
bid_flags(legacy_notices)  # Extract global telemetry flags

Prerequisites

First, ensure you have {shiny.telemetry} set up in your Shiny application:

library(shiny)
library(shiny.telemetry)

# Initialize telemetry
telemetry <- Telemetry$new()

ui <- fluidPage(
  use_telemetry(), # Add telemetry JavaScript

  titlePanel("Sales Dashboard"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "region",
        "Region:",
        choices = c("North", "South", "East", "West")
      ),
      dateRangeInput("date_range", "Date Range:"),
      selectInput(
        "product_category",
        "Product Category:",
        choices = c("All", "Electronics", "Clothing", "Food")
      ),
      actionButton("refresh", "Refresh Data")
    ),
    mainPanel(
      tabsetPanel(
        tabPanel("Overview", plotOutput("overview_plot")),
        tabPanel("Details", dataTableOutput("details_table")),
        tabPanel("Settings", uiOutput("settings_ui"))
      )
    )
  )
)

server <- function(input, output, session) {
  # Start telemetry tracking
  telemetry$start_session()

  # Your app logic here...
}

shinyApp(ui, server)

Analyzing Telemetry Data

After collecting telemetry data from your users, use bid_ingest_telemetry() to identify UX issues:

library(bidux)

# Analyze telemetry from SQLite database (default)
issues <- bid_ingest_telemetry("telemetry.sqlite")

# Or from JSON log file
issues <- bid_ingest_telemetry("telemetry.log", format = "json")

# Review identified issues
length(issues)
names(issues)

Understanding the Analysis

The function analyzes five key friction indicators:

1. Unused or Under-used Inputs

Identifies UI controls that users rarely or never interact with:

# Example: Region filter is never used
issues$unused_input_region
#> BID Framework - Notice Stage
#> Problem: Users are not interacting with the 'region' input control
#> Theory: Hick's Law (auto-suggested)
#> Evidence: Telemetry shows 0 out of 847 sessions where 'region' was changed

This suggests the region filter might be:

2. Delayed First Interactions

Detects when users take too long to start using the dashboard:

issues$delayed_interaction
#> BID Framework - Notice Stage
#> Problem: Users take a long time before making their first interaction with the dashboard
#> Theory: Information Scent (auto-suggested)
#> Evidence: Median time to first input is 45 seconds, and 10% of sessions had no interactions at all

This indicates users might be:

3. Frequent Errors

Identifies systematic errors that disrupt user experience:

issues$error_1
#> BID Framework - Notice Stage
#> Problem: Users encounter errors when using the dashboard
#> Theory: Norman's Gulf of Evaluation (auto-suggested)
#> Evidence: Error 'Data query failed' occurred 127 times in 15.0% of sessions (in output 'overview_plot'), often after changing 'date_range'

This reveals:

5. Confusion Patterns

Detects rapid repeated changes indicating user confusion:

issues$confusion_date_range
#> BID Framework - Notice Stage
#> Problem: Users show signs of confusion when interacting with 'date_range'
#> Theory: Feedback Loops (auto-suggested)
#> Evidence: 8 sessions showed rapid repeated changes (avg 6 changes in 7.5 seconds), suggesting users are unsure about the input's behavior

This suggests:

Customizing Analysis Thresholds

You can adjust the sensitivity of the analysis:

issues <- bid_ingest_telemetry(
  "telemetry.sqlite",
  thresholds = list(
    unused_input_threshold = 0.1, # Flag if <10% of sessions use input
    delay_threshold_seconds = 60, # Flag if >60s before first interaction
    error_rate_threshold = 0.05, # Flag if >5% of sessions have errors
    navigation_threshold = 0.3, # Flag if <30% visit a page
    rapid_change_window = 5, # Look for 5 changes within...
    rapid_change_count = 3 # ...3 seconds
  )
)

Bridge Functions and Sugar Syntax (0.3.1+)

The new API provides convenient bridge functions to connect telemetry issues with BID stages:

Individual Issue Handling

# Process a single high-priority issue
priority_issue <- issues |>
  filter(severity == "critical") |>
  slice_head(n = 1)

# Create Notice directly from issue
notice_result <- bid_notice_issue(
  issue = priority_issue,
  previous_stage = interpret_result,
  override = list(
    problem = "Custom problem description if needed"
  )
)

# Sugar function for quick addressing
notice_sugar <- bid_address(
  issue = priority_issue,
  previous_stage = interpret_result
)

Batch Processing

# Process multiple issues at once
high_priority <- issues |>
  filter(severity %in% c("critical", "high"))

# Convert all to Notice stages
notice_list <- bid_notices(
  issues = high_priority,
  previous_stage = interpret_result,
  filter = function(x) x$severity == "critical"  # Additional filtering
)

# Pipeline approach - process first N issues
pipeline_notices <- bid_pipeline(
  issues = high_priority,
  previous_stage = interpret_result,
  max = 3  # Limit to 3 most critical issues
)

Telemetry-Informed Structure Selection

# Extract global flags from telemetry data
flags <- bid_flags(issues)
#> List includes: has_navigation_issues, high_error_rate, user_confusion_patterns

# Use flags to inform layout selection
structure_result <- bid_structure(
  previous_stage = anticipate_result,
  telemetry_flags = flags  # Influences layout choice and suggestion scoring
)

# The layout selection will avoid problematic patterns based on your data
# e.g., if flags$has_navigation_issues == TRUE, tabs layout gets lower priority

Integrating with BID Workflow

Use the identified issues to drive your BID process:

# Take the most critical issue
critical_issue <- issues$error_1

# Start with interpretation
interpret_result <- bid_interpret(
  central_question = "How can we prevent data query errors?",
  data_story = list(
    hook = "15% of users encounter errors",
    context = "Errors occur after date range changes",
    tension = "Users lose trust when queries fail",
    resolution = "Implement robust error handling and loading states"
  )
)

# Notice the specific problem
notice_result <- bid_notice(
  previous_stage = interpret_result,
  problem = critical_issue$problem,
  evidence = critical_issue$evidence
)

# Anticipate user behavior and biases
anticipate_result <- bid_anticipate(
  previous_stage = notice_result,
  bias_mitigations = list(
    anchoring = "Show loading states to set proper expectations",
    confirmation_bias = "Display error context to help users understand issues"
  )
)

# Structure improvements
structure_result <- bid_structure(
  previous_stage = anticipate_result
)

# Validate and provide next steps
validate_result <- bid_validate(
  previous_stage = structure_result,
  summary_panel = "Error handling improvements with clear user feedback",
  next_steps = c(
    "Implement loading states",
    "Add error context",
    "Test with users"
  )
)

Real-World Example: E-commerce Dashboard Optimization

Let’s walk through a complete example of using telemetry data to improve an e-commerce dashboard. This shows how to go from raw user behavior data to concrete UX improvements.

The Scenario

You’ve built an e-commerce analytics dashboard for business stakeholders. After 3 months in production, users are complaining it’s “hard to use” but can’t be specific about what’s wrong.

Step 1: Diagnose with Telemetry Data

# Analyze 3 months of user behavior data
issues <- bid_telemetry("ecommerce_dashboard_telemetry.sqlite")

# Review the systematic analysis
print(issues)
#> # BID Telemetry Issues: 8 issues identified
#> # Severity breakdown: 2 critical, 3 high, 2 medium, 1 low
#> #
#> # Critical Issues:
#> # 1. delayed_interaction: Users take 47 seconds before first interaction
#> # 2. unused_input_advanced_filters: Only 3% of users interact with advanced filter panel
#> #
#> # High Priority Issues:
#> # 3. error_data_loading: 23% of sessions encounter "Data query failed" error
#> # 4. navigation_settings_tab: Only 8% visit settings, 85% of those sessions end there
#> # 5. confusion_date_range: Rapid repeated changes suggest user confusion

# Examine the most critical issue in detail
critical_issue <- issues |>
  filter(severity == "critical") |>
  slice_head(n = 1)

print(critical_issue$evidence)
#> "Median time to first input is 47 seconds, and 12% of sessions had no interactions at all.
#>  This suggests users are overwhelmed by the initial interface or unclear about where to start."

Step 2: Apply BID Framework Systematically

# Start with interpretation of the business context
interpret_stage <- bid_interpret(
  central_question = "How can we make e-commerce insights more accessible to busy stakeholders?",
  data_story = list(
    hook = "Business teams struggle to get quick insights from our analytics dashboard",
    context = "Stakeholders have 10-15 minutes between meetings to check performance",
    tension = "Current interface requires 47+ seconds just to orient and start using",
    resolution = "Provide immediate value with progressive disclosure for deeper analysis"
  ),
  user_personas = list(
    list(
      name = "Marketing Manager",
      goals = "Quick campaign performance insights",
      pain_points = "Too much information, unclear where to start",
      technical_level = "Intermediate"
    ),
    list(
      name = "Executive",
      goals = "High-level business health check",
      pain_points = "Gets lost in technical details",
      technical_level = "Basic"
    )
  )
)

# Convert telemetry issues to BID Notice stages using bridge functions
notice_stages <- bid_notices(
  issues = critical_issue,
  previous_stage = interpret_stage
)

# Apply behavioral science to anticipate user behavior
anticipate_stage <- bid_anticipate(
  previous_stage = notice_stages[[1]],
  bias_mitigations = list(
    choice_overload = "Reduce initial options, use progressive disclosure",
    attention_bias = "Use visual hierarchy to guide user focus",
    anchoring = "Lead with most important business metric"
  )
)

# Use telemetry flags to inform structure decisions
flags <- bid_flags(issues)
structure_stage <- bid_structure(
  previous_stage = anticipate_stage,
  telemetry_flags = flags  # This influences layout selection
)

# Define success criteria and validation approach
validate_stage <- bid_validate(
  previous_stage = structure_stage,
  summary_panel = "Executive summary with key insights and trend indicators",
  collaboration = "Enable stakeholders to share insights and add context",
  next_steps = c(
    "Implement simplified landing page with key metrics",
    "Add progressive disclosure for detailed analytics",
    "Create role-based views for different user types",
    "Set up telemetry tracking to measure improvement"
  )
)

Step 3: Implement Evidence-Based Improvements

# Before: Information overload (what telemetry revealed users struggled with)
ui_before <- dashboardPage(
  dashboardHeader(title = "E-commerce Analytics"),
  dashboardSidebar(
    # 15+ filter options immediately visible
    selectInput("date_range", "Date Range", choices = date_options),
    selectInput("product_category", "Category", choices = categories, multiple = TRUE),
    selectInput("channel", "Sales Channel", choices = channels, multiple = TRUE),
    selectInput("region", "Region", choices = regions, multiple = TRUE),
    selectInput("customer_segment", "Customer Type", choices = segments, multiple = TRUE),
    # ... 10 more filters
    actionButton("apply_filters", "Apply Filters")
  ),
  dashboardBody(
    # 12 value boxes competing for attention
    fluidRow(
      valueBoxOutput("revenue"), valueBoxOutput("orders"),
      valueBoxOutput("aov"), valueBoxOutput("conversion"),
      valueBoxOutput("traffic"), valueBoxOutput("bounce_rate"),
      # ... 6 more value boxes
    ),
    # Multiple complex charts
    fluidRow(
      plotOutput("revenue_trend"), plotOutput("category_performance"),
      plotOutput("channel_analysis"), plotOutput("customer_segments")
    )
  )
)

# After: BID-informed design addressing telemetry insights
ui_after <- page_fillable(
  theme = bs_theme(version = 5, preset = "bootstrap"),

  # Address delayed interaction issue: Immediate value on landing
  layout_columns(
    col_widths = c(8, 4),

    # Primary business health (addresses anchoring bias)
    card(
      card_header("🎯 Business Performance", class = "bg-primary text-white"),
      layout_columns(
        col_widths = c(6, 6),
        value_box(
          "Revenue Today",
          "$47.2K",
          "vs. $43.1K yesterday (+9.5%)",
          showcase = bs_icon("graph-up"),
          theme = "success"
        ),
        div(
          h5("Key Insights", style = "margin-bottom: 15px;"),
          tags$ul(
            tags$li("Mobile traffic up 15%"),
            tags$li("Electronics category leading"),
            tags$li("⚠️ Cart abandonment rate increased")
          ),
          actionButton("investigate_abandonment", "Investigate",
                      class = "btn btn-warning btn-sm")
        )
      )
    ),

    # Quick actions (addresses choice overload)
    card(
      card_header("⚡ Quick Actions"),
      div(
        actionButton(
          "todays_performance",
          "Today's Performance",
          class = "btn btn-primary btn-block mb-2"
        ),
        actionButton(
          "weekly_trends",
          "Weekly Trends",
          class = "btn btn-secondary btn-block mb-2"
        ),
        actionButton(
          "campaign_results",
          "Campaign Results",
          class = "btn btn-info btn-block mb-2"
        ),
        hr(),
        p(
          "Need something specific?",
          style = "font-size: 0.9em; color: #666;"
        ),
        actionButton(
          "show_filters",
          "Custom Analysis",
          class = "btn btn-outline-secondary btn-sm"
        )
      )
    )
  ),

  # Advanced options hidden by default (progressive disclosure)
  conditionalPanel(
    condition = "input.show_filters",
    card(
      card_header("🔍 Custom Analysis"),
      p("Filter and explore your data:", style = "margin-bottom: 15px;"),
      layout_columns(
        col_widths = c(3, 3, 3, 3),
        selectInput(
          "time_period",
          "Time Period",
          choices = c("Today", "This Week", "This Month"),
          selected = "Today"
        ),
        selectInput(
          "focus_area",
          "Focus Area",
          choices = c("Revenue", "Traffic", "Conversions", "Customers")
        ),
        selectInput(
          "comparison",
          "Compare To",
          choices = c("Previous Period", "Same Period Last Year", "Target")
        ),
        actionButton("apply_custom", "Analyze", class = "btn btn-primary")
      )
    )
  ),

  # Results area appears based on user choices
  div(id = "results_area", style = "margin-top: 20px;")
)

Step 4: Measure the Impact

# After implementing changes, collect new telemetry data
issues_after <- bid_telemetry(
  "ecommerce_dashboard_telemetry_after_changes.sqlite"
)

# Compare before/after metrics
improvement_metrics <- tibble(
  metric = c(
    "Time to first interaction",
    "Session abandonment rate",
    "User satisfaction score",
    "Task completion rate"
  ),
  before = c("47 seconds", "12%", "6.2/10", "68%"),
  after = c("8 seconds", "3%", "8.1/10", "87%"),
  improvement = c("-83%", "-75%", "+31%", "+28%")
)

print(improvement_metrics)

Example: Complete Analysis Using 0.3.1 API

Here’s the complete workflow using the streamlined modern API:

# 1. Modern telemetry ingestion
issues <- bid_telemetry("telemetry.sqlite")

# 2. Triage issues using tidy workflow
print(issues)  # Review issue summary

# Focus on critical issues
critical_issues <- issues |>
  filter(severity == "critical") |>
  arrange(desc(user_impact))

# 3. Create a comprehensive improvement plan using bridge functions
if (nrow(critical_issues) > 0) {
  # Address top critical issue
  top_issue <- critical_issues |> slice_head(n = 1)

  # Start BID workflow
  interpret_stage <- bid_interpret(
    central_question = "How can we address the most critical UX issue?",
    data_story = list(
      hook = "Critical issues identified from user behavior data",
      context = "Telemetry reveals specific friction points",
      tension = "Users struggling with core functionality",
      resolution = "Data-driven improvements to user experience"
    )
  )

  # Use bridge function to convert issue to Notice
  notice_stage <- bid_notice_issue(
    issue = top_issue,
    previous_stage = interpret_stage
  )

  improvement_plan <- notice_stage |>
    bid_anticipate(
      bias_mitigations = list(
        choice_overload = "Hide advanced filters until needed",
        default_effect = "Pre-select most common filter values"
      )
    ) |>
    bid_structure(telemetry_flags = bid_flags(issues)) |>
    bid_validate(
      summary_panel = "Simplified filtering with progressive disclosure",
      next_steps = c(
        "Remove unused filters",
        "Implement progressive disclosure",
        "Add contextual help",
        "Re-test with telemetry after changes"
      )
    )
}

# 4. Generate report
improvement_report <- bid_report(improvement_plan, format = "html")

Best Practices

  1. Collect Sufficient Data: Ensure you have telemetry from at least 50-100 sessions before analysis for reliable patterns.

  2. Regular Analysis: Run telemetry analysis periodically (e.g., monthly) to catch emerging issues.

  3. Combine with Qualitative Data: Use telemetry insights alongside user interviews and usability testing.

  4. Track Improvements: After implementing changes, collect new telemetry to verify improvements:

# Before changes
issues_before <- bid_ingest_telemetry("telemetry_before.sqlite")

# After implementing improvements
issues_after <- bid_ingest_telemetry("telemetry_after.sqlite")

# Compare issue counts
cat("Issues before:", length(issues_before), "\n")
cat("Issues after:", length(issues_after), "\n")
  1. Document Patterns: Build a knowledge base of common patterns in your domain:
# Save recurring patterns for future reference
telemetry_patterns <- list(
  date_filter_confusion = "Users often struggle with date range inputs - consider using presets",
  tab_discovery = "Secondary tabs have low discovery - consider better visual hierarchy",
  error_recovery = "Users abandon after errors - implement graceful error handling"
)

Conclusion

The bid_ingest_telemetry() function bridges the gap between user behavior data and design decisions. By automatically identifying friction points from real usage patterns, it provides concrete, evidence-based starting points for the BID framework, ultimately leading to more user-friendly Shiny applications.