From a4c544d82268d995fef61a7cd312637aa9301bfa Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 17 Jun 2026 11:27:43 -0400 Subject: [PATCH] More metrics on the sync details page Added total count and success rate graphs to the sync details dashboard. Seeing the total count is useful when trying to corrolate success rate changes. Renamed this dashboard to "sync details" since it's now not just about the error list. --- .../src/metrics/sync.rs | 119 +++++++++++++++--- tools/generate-rust-dashboards/src/schema.rs | 23 ++++ 2 files changed, 128 insertions(+), 14 deletions(-) diff --git a/tools/generate-rust-dashboards/src/metrics/sync.rs b/tools/generate-rust-dashboards/src/metrics/sync.rs index ce512dbae2..5c739c4c02 100644 --- a/tools/generate-rust-dashboards/src/metrics/sync.rs +++ b/tools/generate-rust-dashboards/src/metrics/sync.rs @@ -5,9 +5,9 @@ use crate::{ config::{Application, ReleaseChannel, TeamConfig}, schema::{ - CustomVariable, Dashboard, DashboardBuilder, DataLink, Datasource, FieldConfig, - FieldConfigCustom, FieldConfigDefaults, GridPos, LogOptions, LogPanel, Panel, Target, - TextPanel, TimeSeriesPanel, Transformation, + CalculateFieldOptions, CustomVariable, Dashboard, DashboardBuilder, DataLink, Datasource, + FieldConfig, FieldConfigCustom, FieldConfigDefaults, GridPos, LogOptions, LogPanel, Panel, + Target, TextPanel, TimeSeriesPanel, Transformation, WindowFunctionWindow, }, sql::Query, util::{Join, UrlBuilder}, @@ -27,8 +27,8 @@ pub fn add_to_main_dashboard(builder: &mut DashboardBuilder, config: &TeamConfig pub fn extra_dashboard(config: &TeamConfig) -> Result { let mut builder = DashboardBuilder::new( - format!("{} - Sync Errors", config.team_name), - format!("{}-sync-errors", config.team_slug()), + format!("{} - Sync Details", config.team_name), + format!("{}-sync-details", config.team_slug()), ); builder.add_application_variable(config)?; builder.add_channel_variable(); @@ -44,8 +44,20 @@ pub fn extra_dashboard(config: &TeamConfig) -> Result { ..CustomVariable::default() }); - builder.add_panel_full(error_list_count_panel(config)); - builder.add_panel_full(error_list_log_panel(config)); + builder.add_panel_title("Metrics"); + builder.add_panel_full(details_dash_count_panel( + "Success Rate", + "success_rate", + false, + )); + builder.add_panel_full(details_dash_count_panel( + "Total counts (7 day moving average)", + "count_total", + true, + )); + builder.add_panel_title("Errors"); + builder.add_panel_full(details_dash_error_count_panel(config)); + builder.add_panel_full(details_dash_error_log_panel(config)); Ok(builder.dashboard) } @@ -66,7 +78,7 @@ fn overview_count_panel( let query = count_query(config, application, format!("'{channel}'")); - TimeSeriesPanel { + Panel::from(TimeSeriesPanel { title: application.display_name(channel), grid_pos: GridPos::height(8), datasource: Datasource::bigquery(), @@ -76,7 +88,7 @@ fn overview_count_panel( field_config: FieldConfig { defaults: FieldConfigDefaults { links: vec![DataLink { - url: UrlBuilder::new_dashboard(format!("{}-sync-errors", config.team_slug())) + url: UrlBuilder::new_dashboard(format!("{}-sync-details", config.team_slug())) .with_time_range_param() .with_param("var-application", application.slug()) .with_param("var-channel", channel.to_string()) @@ -107,8 +119,7 @@ fn overview_count_panel( }, ], ..TimeSeriesPanel::default() - } - .into() + }) } /// Query to fetch sync success rates @@ -151,7 +162,87 @@ ORDER BY time" ) } -fn error_list_count_panel(config: &TeamConfig) -> Panel { +fn details_dash_count_panel(title: &str, column_name: &str, moving_average: bool) -> Panel { + let query = Query { + select: vec!["time".into(), column_name.into()], + from: format!("(\n{}\n)", details_dash_count_query()), + group_by: Some("1, 2".into()), + ..Query::default() + }; + + let transformations = if moving_average { + vec![Transformation::CalculateField( + CalculateFieldOptions::WindowFunctions { + replace_fields: true, + window: WindowFunctionWindow { + field: "count_total".into(), + reducer: "mean".into(), + window_alignment: "centered".into(), + window_size: 7.0, + window_size_mode: "fixed".into(), + }, + }, + )] + } else { + vec![] + }; + + TimeSeriesPanel { + title: title.into(), + grid_pos: GridPos::height(10), + datasource: Datasource::bigquery(), + // needs to be fairly large since the total sync count can be low on mobile/nightly + interval: "1d".into(), + targets: vec![Target::table(query.sql())], + transformations, + ..TimeSeriesPanel::default() + } + .into() +} + +/// Query to count metrics for the details dashboard +fn details_dash_count_query() -> String { + "\ +SELECT + TIMESTAMP(submission_date) as time, + success_rate, + count_total +FROM + moz-fx-data-shared-prod.sync_derived.desktop_v1 +WHERE + $__timeFilter(TIMESTAMP(submission_date)) + AND channel = '${channel}' + AND application=CASE '${application}' + WHEN 'firefox_desktop' THEN 'desktop' + WHEN 'firefox_android' THEN 'firefox-android' + WHEN 'firefox_ios' THEN 'firefox-ios' + ELSE '${application}' + END + AND engine_name = '${engine}' + +UNION ALL + +SELECT + TIMESTAMP(submission_date) as time, + success_rate, + count_total +FROM + moz-fx-data-shared-prod.sync_derived.mobile_v1 +WHERE + $__timeFilter(TIMESTAMP(submission_date)) + AND channel = '${channel}' + AND application=CASE '${application}' + WHEN 'firefox_desktop' THEN 'desktop' + WHEN 'firefox_android' THEN 'firefox-android' + WHEN 'firefox_ios' THEN 'firefox-ios' + ELSE '${application}' + END + AND engine_name = '${engine}' +ORDER BY time" + .to_string() +} + +fn details_dash_error_count_panel(config: &TeamConfig) -> Panel { let query = Query { select: vec![ "error".into(), @@ -171,7 +262,7 @@ fn error_list_count_panel(config: &TeamConfig) -> Panel { }; TimeSeriesPanel { - title: "Error counts".into(), + title: "Error counts by type".into(), grid_pos: GridPos::height(10), datasource: Datasource::bigquery(), // needs to be fairly large since the total sync count can be low on mobile/nightly @@ -192,7 +283,7 @@ fn error_list_count_panel(config: &TeamConfig) -> Panel { .into() } -fn error_list_log_panel(config: &TeamConfig) -> Panel { +fn details_dash_error_log_panel(config: &TeamConfig) -> Panel { let query = Query { select: vec![ "CONCAT(IFNULL(error, 'unknown'), ': ', IFNULL(details, 'unknown')) as message".into(), diff --git a/tools/generate-rust-dashboards/src/schema.rs b/tools/generate-rust-dashboards/src/schema.rs index a905e740aa..8e9ab5db3e 100644 --- a/tools/generate-rust-dashboards/src/schema.rs +++ b/tools/generate-rust-dashboards/src/schema.rs @@ -243,6 +243,29 @@ pub enum Transformation { regex: String, rename_pattern: String, }, + #[serde(rename_all = "camelCase")] + CalculateField(CalculateFieldOptions), +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "mode")] +pub enum CalculateFieldOptions { + #[serde(rename_all = "camelCase")] + WindowFunctions { + window: WindowFunctionWindow, + replace_fields: bool, + }, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowFunctionWindow { + pub field: String, + pub reducer: String, + pub window_alignment: String, + pub window_size_mode: String, + pub window_size: f32, } #[derive(Default, Serialize, Clone, Copy)]