From 48d9a2582089b42758ff37e2917d61c9e4eb01a8 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 26 Nov 2025 14:58:17 -0600 Subject: [PATCH 01/24] Fix that 'closed'->'done' change broke this route --- app/models/user/day_timeline/column.rb | 9 +++++++++ app/views/events/day_timeline/_column.html.erb | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/user/day_timeline/column.rb b/app/models/user/day_timeline/column.rb index 0331a83ff..ce87873fa 100644 --- a/app/models/user/day_timeline/column.rb +++ b/app/models/user/day_timeline/column.rb @@ -17,6 +17,15 @@ class User::DayTimeline::Column safe_join(parts, " ") end + def base_title_for_route + case base_title + when "Done" + "closed" + else + base_title.downcase + end + end + def events_by_hour limited_events.group_by { it.created_at.hour } end diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb index 23bf7e34e..509efd69d 100644 --- a/app/views/events/day_timeline/_column.html.erb +++ b/app/views/events/day_timeline/_column.html.erb @@ -3,7 +3,7 @@ <%= column.title %> <% if column.events_by_hour.any? %> - <%= link_to events_day_timeline_column_path(id: column.base_title.downcase, day: column.day_timeline.day.to_date), class: "events__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= link_to events_day_timeline_column_path(id: column.base_title_for_route, day: column.day_timeline.day.to_date), class: "events__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> <%= icon_tag "grid", class: "translucent" %> Expand column <% end %> From 8ec1a5c76085a3756e5826f8a33aa08f10b51579 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 26 Nov 2025 15:38:26 -0600 Subject: [PATCH 02/24] Fix that board button was leaking to the right of columns See: https://app.fizzy.do/5986089/cards/3148 --- app/assets/stylesheets/header.css | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css index 59d94a0f3..21553e358 100644 --- a/app/assets/stylesheets/header.css +++ b/app/assets/stylesheets/header.css @@ -42,7 +42,6 @@ font-size: var(--text-x-small); gap: var(--header-gap); inline-size: var(--header-actions-width); - min-inline-size: fit-content; } .header__actions--start { From d46761d89c35a289b9c7853880240588759a78f6 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 26 Nov 2025 16:03:02 -0600 Subject: [PATCH 03/24] Set entropy to 1 year so cards don't close before they can play --- app/models/account/seeder.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index f7f7cf60b..9858cff6b 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -25,6 +25,7 @@ class Account::Seeder # Playground Board # --------------- playground = account.boards.create! name: "Playground", creator: creator, all_access: true + playground.update! auto_postpone_period: 365.days # Cards playground.cards.create! creator: creator, title: "Finally, watch this Fizzy orientation video", status: "published", description: <<~HTML From e8671d11fafd609fdc907b734e128f00307353f6 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Nov 2025 13:56:53 -0500 Subject: [PATCH 04/24] Improve AGENTS.md with some loki suggestions Suggested by Claude after I yelled at it when it was using string matching instead of the structured fields. --- AGENTS.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e8d1f5d63..51407a6fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,10 +152,40 @@ Grafana MCP tools provide access to production metrics and logs for performance - `rails_request_total:rate1m:sum_by_controller_action{app="fizzy"}` - Request rates by endpoint - `fizzy_replica_wait_seconds` - Database replica consistency wait times -### Loki Log Labels -Query: `{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"}` +### Loki Log Labels and Query Patterns -Useful fields: `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller` +**Base label selector:** +```logql +{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"} +``` + +**Useful JSON fields:** `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller`, `url_path`, `authentication_identity_id`, `http_response_status_code` + +**Query patterns:** +- Filter by fields: `{labels} | field_name = "value"` +- Multiple field filters: `{labels} | field1 = "value1" | field2 = "value2"` +- Reduce returned labels: `{labels} | filters | keep field1,field2,field3` (reduces label payload) +- Minimize log line content: `{labels} | filters | line_format "{{.field_name}}"` (replaces raw log line) +- Combine both for minimal tokens: `{labels} | filters | keep field1,field2 | line_format "{{.field1}}"` +- **Important:** Fields are pre-parsed by the OTel collector. Don't use string search (`|=`) when filtering structured fields +- **Important:** Do NOT use `| json` - it will cause JSONParserErr since fields are already parsed as labels + +**Token management (CRITICAL):** +- Always probe with `limit: 3` first to check response size before running larger queries +- Aggregations return time series (many data points), not single values - can explode token usage +- NEVER use `sum by (field)` - returns a time series per unique value, easily exceeds token limits +- For breakdowns by field: fetch raw logs with `| keep field | line_format "{{.field}}"` and count client-side + +**Aggregations for statistics (use instead of fetching raw logs):** +- `mcp__grafana__query_loki_logs` returns limited results (default 10, max ~100) and large responses get truncated; use aggregations for statistics on large datasets +- Count: `sum(count_over_time({labels} | filters [12h]))` +- Percentiles: `quantile_over_time(0.95, {labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Average: `avg_over_time({labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Min/Max: `min_over_time(...)` / `max_over_time(...)` +- The `| unwrap field_name | __error__=""` pattern extracts numeric values from pre-parsed labels +- Use `by ()` or wrap in `sum()` to avoid cardinality limits + +**Documentation:** For advanced LogQL syntax (aggregations, pattern matching, etc.), consult https://grafana.com/docs/loki/latest/query/ ### Instrumentation Yabeda-based metrics exported at `:9394/metrics`. Config in `config/initializers/yabeda.rb`. From fced0a48ee7303fc9ac59d3c6bf6f8c078abe8dc Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Nov 2025 21:32:38 -0500 Subject: [PATCH 05/24] Rename bin/gitleaks to gitleaks-audit to prevent script recursion if bin is in user's $PATH --- bin/{gitleaks => gitleaks-audit} | 0 config/ci.rb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename bin/{gitleaks => gitleaks-audit} (100%) diff --git a/bin/gitleaks b/bin/gitleaks-audit similarity index 100% rename from bin/gitleaks rename to bin/gitleaks-audit diff --git a/config/ci.rb b/config/ci.rb index b7ddab694..f6cbbf953 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -8,7 +8,7 @@ CI.run do step "Security: Gem audit", "bin/bundler-audit check --update" step "Security: Importmap audit", "bin/importmap audit" step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" - step "Security: Gitleaks audit", "bin/gitleaks" + step "Security: Gitleaks audit", "bin/gitleaks-audit" step "Tests: Rails: SaaS config", "bin/rails test" step "Tests: Rails: OSS config", "OSS_CONFIG=1 bin/rails test" From e44524ebc336f46b12143943598da97c00b5061b Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:07:02 +0100 Subject: [PATCH 06/24] Prevent Fizzy menu from closing on page refreshes https://app.fizzy.do/5986089/cards/3190 --- app/views/my/_menu.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/my/_menu.html.erb b/app/views/my/_menu.html.erb index 2b5c1d0ea..31d3d2551 100644 --- a/app/views/my/_menu.html.erb +++ b/app/views/my/_menu.html.erb @@ -14,7 +14,8 @@ controller: "filter navigable-list nav-section-expander", dialog_target: "dialog", navigable_list_focus_on_selection_value: false, - navigable_list_actionable_items_value: true } do %> + navigable_list_actionable_items_value: true, + turbo_permanent: true } do %> <%= turbo_frame_tag "my_menu", src: my_menu_path, loading: :lazy, target: "_top" do %> <% # Passing empty block to avoid double-render %> <%= render("my/menus/jump") { } %> From 41dfcd6beea16d396c60b53c2694c7287770b465 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Nov 2025 13:56:53 -0500 Subject: [PATCH 07/24] Improve AGENTS.md with some loki suggestions Suggested by Claude after I yelled at it when it was using string matching instead of the structured fields. --- AGENTS.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e8d1f5d63..51407a6fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,10 +152,40 @@ Grafana MCP tools provide access to production metrics and logs for performance - `rails_request_total:rate1m:sum_by_controller_action{app="fizzy"}` - Request rates by endpoint - `fizzy_replica_wait_seconds` - Database replica consistency wait times -### Loki Log Labels -Query: `{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"}` +### Loki Log Labels and Query Patterns -Useful fields: `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller` +**Base label selector:** +```logql +{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"} +``` + +**Useful JSON fields:** `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller`, `url_path`, `authentication_identity_id`, `http_response_status_code` + +**Query patterns:** +- Filter by fields: `{labels} | field_name = "value"` +- Multiple field filters: `{labels} | field1 = "value1" | field2 = "value2"` +- Reduce returned labels: `{labels} | filters | keep field1,field2,field3` (reduces label payload) +- Minimize log line content: `{labels} | filters | line_format "{{.field_name}}"` (replaces raw log line) +- Combine both for minimal tokens: `{labels} | filters | keep field1,field2 | line_format "{{.field1}}"` +- **Important:** Fields are pre-parsed by the OTel collector. Don't use string search (`|=`) when filtering structured fields +- **Important:** Do NOT use `| json` - it will cause JSONParserErr since fields are already parsed as labels + +**Token management (CRITICAL):** +- Always probe with `limit: 3` first to check response size before running larger queries +- Aggregations return time series (many data points), not single values - can explode token usage +- NEVER use `sum by (field)` - returns a time series per unique value, easily exceeds token limits +- For breakdowns by field: fetch raw logs with `| keep field | line_format "{{.field}}"` and count client-side + +**Aggregations for statistics (use instead of fetching raw logs):** +- `mcp__grafana__query_loki_logs` returns limited results (default 10, max ~100) and large responses get truncated; use aggregations for statistics on large datasets +- Count: `sum(count_over_time({labels} | filters [12h]))` +- Percentiles: `quantile_over_time(0.95, {labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Average: `avg_over_time({labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Min/Max: `min_over_time(...)` / `max_over_time(...)` +- The `| unwrap field_name | __error__=""` pattern extracts numeric values from pre-parsed labels +- Use `by ()` or wrap in `sum()` to avoid cardinality limits + +**Documentation:** For advanced LogQL syntax (aggregations, pattern matching, etc.), consult https://grafana.com/docs/loki/latest/query/ ### Instrumentation Yabeda-based metrics exported at `:9394/metrics`. Config in `config/initializers/yabeda.rb`. From 06730cbbb9055b50c334f7d0532ea4a00195cb8f Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Nov 2025 21:32:38 -0500 Subject: [PATCH 08/24] Rename bin/gitleaks to gitleaks-audit to prevent script recursion if bin is in user's $PATH --- bin/{gitleaks => gitleaks-audit} | 0 config/ci.rb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename bin/{gitleaks => gitleaks-audit} (100%) diff --git a/bin/gitleaks b/bin/gitleaks-audit similarity index 100% rename from bin/gitleaks rename to bin/gitleaks-audit diff --git a/config/ci.rb b/config/ci.rb index b7ddab694..f6cbbf953 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -8,7 +8,7 @@ CI.run do step "Security: Gem audit", "bin/bundler-audit check --update" step "Security: Importmap audit", "bin/importmap audit" step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" - step "Security: Gitleaks audit", "bin/gitleaks" + step "Security: Gitleaks audit", "bin/gitleaks-audit" step "Tests: Rails: SaaS config", "bin/rails test" step "Tests: Rails: OSS config", "OSS_CONFIG=1 bin/rails test" From 67c77e1c23006bbd598d1859e6040d95efb15957 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:07:02 +0100 Subject: [PATCH 09/24] Prevent Fizzy menu from closing on page refreshes https://app.fizzy.do/5986089/cards/3190 --- app/views/my/_menu.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/my/_menu.html.erb b/app/views/my/_menu.html.erb index 2b5c1d0ea..31d3d2551 100644 --- a/app/views/my/_menu.html.erb +++ b/app/views/my/_menu.html.erb @@ -14,7 +14,8 @@ controller: "filter navigable-list nav-section-expander", dialog_target: "dialog", navigable_list_focus_on_selection_value: false, - navigable_list_actionable_items_value: true } do %> + navigable_list_actionable_items_value: true, + turbo_permanent: true } do %> <%= turbo_frame_tag "my_menu", src: my_menu_path, loading: :lazy, target: "_top" do %> <% # Passing empty block to avoid double-render %> <%= render("my/menus/jump") { } %> From 9d9c0ab2f3eac616382e365b7e2b0fe3892e7cee Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Nov 2025 13:56:53 -0500 Subject: [PATCH 10/24] Improve AGENTS.md with some loki suggestions Suggested by Claude after I yelled at it when it was using string matching instead of the structured fields. --- AGENTS.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e8d1f5d63..51407a6fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,10 +152,40 @@ Grafana MCP tools provide access to production metrics and logs for performance - `rails_request_total:rate1m:sum_by_controller_action{app="fizzy"}` - Request rates by endpoint - `fizzy_replica_wait_seconds` - Database replica consistency wait times -### Loki Log Labels -Query: `{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"}` +### Loki Log Labels and Query Patterns -Useful fields: `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller` +**Base label selector:** +```logql +{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"} +``` + +**Useful JSON fields:** `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller`, `url_path`, `authentication_identity_id`, `http_response_status_code` + +**Query patterns:** +- Filter by fields: `{labels} | field_name = "value"` +- Multiple field filters: `{labels} | field1 = "value1" | field2 = "value2"` +- Reduce returned labels: `{labels} | filters | keep field1,field2,field3` (reduces label payload) +- Minimize log line content: `{labels} | filters | line_format "{{.field_name}}"` (replaces raw log line) +- Combine both for minimal tokens: `{labels} | filters | keep field1,field2 | line_format "{{.field1}}"` +- **Important:** Fields are pre-parsed by the OTel collector. Don't use string search (`|=`) when filtering structured fields +- **Important:** Do NOT use `| json` - it will cause JSONParserErr since fields are already parsed as labels + +**Token management (CRITICAL):** +- Always probe with `limit: 3` first to check response size before running larger queries +- Aggregations return time series (many data points), not single values - can explode token usage +- NEVER use `sum by (field)` - returns a time series per unique value, easily exceeds token limits +- For breakdowns by field: fetch raw logs with `| keep field | line_format "{{.field}}"` and count client-side + +**Aggregations for statistics (use instead of fetching raw logs):** +- `mcp__grafana__query_loki_logs` returns limited results (default 10, max ~100) and large responses get truncated; use aggregations for statistics on large datasets +- Count: `sum(count_over_time({labels} | filters [12h]))` +- Percentiles: `quantile_over_time(0.95, {labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Average: `avg_over_time({labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Min/Max: `min_over_time(...)` / `max_over_time(...)` +- The `| unwrap field_name | __error__=""` pattern extracts numeric values from pre-parsed labels +- Use `by ()` or wrap in `sum()` to avoid cardinality limits + +**Documentation:** For advanced LogQL syntax (aggregations, pattern matching, etc.), consult https://grafana.com/docs/loki/latest/query/ ### Instrumentation Yabeda-based metrics exported at `:9394/metrics`. Config in `config/initializers/yabeda.rb`. From 980e4972420f9d159b79a53461bf8d61163f9404 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 26 Nov 2025 16:03:02 -0600 Subject: [PATCH 11/24] Set entropy to 1 year so cards don't close before they can play --- app/models/account/seeder.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index f7f7cf60b..9858cff6b 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -25,6 +25,7 @@ class Account::Seeder # Playground Board # --------------- playground = account.boards.create! name: "Playground", creator: creator, all_access: true + playground.update! auto_postpone_period: 365.days # Cards playground.cards.create! creator: creator, title: "Finally, watch this Fizzy orientation video", status: "published", description: <<~HTML From 887f100c5086d6bec4933040943ef09c0da67393 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Nov 2025 21:32:38 -0500 Subject: [PATCH 12/24] Rename bin/gitleaks to gitleaks-audit to prevent script recursion if bin is in user's $PATH --- bin/{gitleaks => gitleaks-audit} | 0 config/ci.rb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename bin/{gitleaks => gitleaks-audit} (100%) diff --git a/bin/gitleaks b/bin/gitleaks-audit similarity index 100% rename from bin/gitleaks rename to bin/gitleaks-audit diff --git a/config/ci.rb b/config/ci.rb index b7ddab694..f6cbbf953 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -8,7 +8,7 @@ CI.run do step "Security: Gem audit", "bin/bundler-audit check --update" step "Security: Importmap audit", "bin/importmap audit" step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" - step "Security: Gitleaks audit", "bin/gitleaks" + step "Security: Gitleaks audit", "bin/gitleaks-audit" step "Tests: Rails: SaaS config", "bin/rails test" step "Tests: Rails: OSS config", "OSS_CONFIG=1 bin/rails test" From 0453526bf1b1d38ba232ae70085e2275ea0f3f94 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:07:02 +0100 Subject: [PATCH 13/24] Prevent Fizzy menu from closing on page refreshes https://app.fizzy.do/5986089/cards/3190 --- app/views/my/_menu.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/my/_menu.html.erb b/app/views/my/_menu.html.erb index 2b5c1d0ea..31d3d2551 100644 --- a/app/views/my/_menu.html.erb +++ b/app/views/my/_menu.html.erb @@ -14,7 +14,8 @@ controller: "filter navigable-list nav-section-expander", dialog_target: "dialog", navigable_list_focus_on_selection_value: false, - navigable_list_actionable_items_value: true } do %> + navigable_list_actionable_items_value: true, + turbo_permanent: true } do %> <%= turbo_frame_tag "my_menu", src: my_menu_path, loading: :lazy, target: "_top" do %> <% # Passing empty block to avoid double-render %> <%= render("my/menus/jump") { } %> From 2edf79672e56fec4cf2ca32062633f6784ef00f0 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Nov 2025 13:56:53 -0500 Subject: [PATCH 14/24] Improve AGENTS.md with some loki suggestions Suggested by Claude after I yelled at it when it was using string matching instead of the structured fields. --- AGENTS.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e8d1f5d63..51407a6fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,10 +152,40 @@ Grafana MCP tools provide access to production metrics and logs for performance - `rails_request_total:rate1m:sum_by_controller_action{app="fizzy"}` - Request rates by endpoint - `fizzy_replica_wait_seconds` - Database replica consistency wait times -### Loki Log Labels -Query: `{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"}` +### Loki Log Labels and Query Patterns -Useful fields: `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller` +**Base label selector:** +```logql +{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"} +``` + +**Useful JSON fields:** `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller`, `url_path`, `authentication_identity_id`, `http_response_status_code` + +**Query patterns:** +- Filter by fields: `{labels} | field_name = "value"` +- Multiple field filters: `{labels} | field1 = "value1" | field2 = "value2"` +- Reduce returned labels: `{labels} | filters | keep field1,field2,field3` (reduces label payload) +- Minimize log line content: `{labels} | filters | line_format "{{.field_name}}"` (replaces raw log line) +- Combine both for minimal tokens: `{labels} | filters | keep field1,field2 | line_format "{{.field1}}"` +- **Important:** Fields are pre-parsed by the OTel collector. Don't use string search (`|=`) when filtering structured fields +- **Important:** Do NOT use `| json` - it will cause JSONParserErr since fields are already parsed as labels + +**Token management (CRITICAL):** +- Always probe with `limit: 3` first to check response size before running larger queries +- Aggregations return time series (many data points), not single values - can explode token usage +- NEVER use `sum by (field)` - returns a time series per unique value, easily exceeds token limits +- For breakdowns by field: fetch raw logs with `| keep field | line_format "{{.field}}"` and count client-side + +**Aggregations for statistics (use instead of fetching raw logs):** +- `mcp__grafana__query_loki_logs` returns limited results (default 10, max ~100) and large responses get truncated; use aggregations for statistics on large datasets +- Count: `sum(count_over_time({labels} | filters [12h]))` +- Percentiles: `quantile_over_time(0.95, {labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Average: `avg_over_time({labels} | filters | unwrap field_name | __error__="" [12h]) by ()` +- Min/Max: `min_over_time(...)` / `max_over_time(...)` +- The `| unwrap field_name | __error__=""` pattern extracts numeric values from pre-parsed labels +- Use `by ()` or wrap in `sum()` to avoid cardinality limits + +**Documentation:** For advanced LogQL syntax (aggregations, pattern matching, etc.), consult https://grafana.com/docs/loki/latest/query/ ### Instrumentation Yabeda-based metrics exported at `:9394/metrics`. Config in `config/initializers/yabeda.rb`. From 5e971f41af3b145900663e20c1b57dd93e21ee83 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 26 Nov 2025 16:03:02 -0600 Subject: [PATCH 15/24] Set entropy to 1 year so cards don't close before they can play --- app/models/account/seeder.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index f7f7cf60b..9858cff6b 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -25,6 +25,7 @@ class Account::Seeder # Playground Board # --------------- playground = account.boards.create! name: "Playground", creator: creator, all_access: true + playground.update! auto_postpone_period: 365.days # Cards playground.cards.create! creator: creator, title: "Finally, watch this Fizzy orientation video", status: "published", description: <<~HTML From 29e62ac90ca7e9c049e57b1aaa1aabf88f99f3bb Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 26 Nov 2025 15:38:26 -0600 Subject: [PATCH 16/24] Fix that board button was leaking to the right of columns See: https://app.fizzy.do/5986089/cards/3148 --- app/assets/stylesheets/header.css | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css index 59d94a0f3..21553e358 100644 --- a/app/assets/stylesheets/header.css +++ b/app/assets/stylesheets/header.css @@ -42,7 +42,6 @@ font-size: var(--text-x-small); gap: var(--header-gap); inline-size: var(--header-actions-width); - min-inline-size: fit-content; } .header__actions--start { From b78977f563f5e63fdaee0570a180b7d7fb698eea Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 27 Nov 2025 10:46:48 -0500 Subject: [PATCH 17/24] Make db/seeds.rb rerunnable when I want to add more cards. --- db/seeds.rb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 5e2ab12ac..80523e86e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -16,16 +16,18 @@ else email_address = "david@37signals.com" identity = Identity.find_or_create_by!(email_address: email_address) - account = Account.create_with_admin_user( - account: { - external_account_id: tenant_id, - name: signal_account_name - }, - owner: { - name: "David Heinemeier Hansson", - identity: identity - } - ) + unless account = Account.find_by(external_account_id: tenant_id) + account = Account.create_with_admin_user( + account: { + external_account_id: tenant_id, + name: signal_account_name + }, + owner: { + name: "David Heinemeier Hansson", + identity: identity + } + ) + end Current.account = account end @@ -43,7 +45,7 @@ else end def create_board(name, creator: Current.user, all_access: true, access_to: []) - Board.create!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) } + Board.find_or_create_by!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) } end def create_card(title, board:, description: nil, status: :published, creator: Current.user) From 79c9ea2ce15897c59ffb2abe83ac2cfa9525bb94 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 27 Nov 2025 11:08:45 -0500 Subject: [PATCH 18/24] Remove a bunch of N+1s related to notification and comment rendering --- app/controllers/notifications/trays_controller.rb | 2 +- app/models/card/assignable.rb | 2 +- app/models/comment.rb | 3 ++- app/models/notification.rb | 2 +- app/views/cards/_messages.html.erb | 2 +- app/views/cards/comments/reactions/_reactions.html.erb | 2 +- test/controllers/cards/assignments_controller_test.rb | 4 ++-- test/models/card_test.rb | 2 +- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/controllers/notifications/trays_controller.rb b/app/controllers/notifications/trays_controller.rb index 26a1f06e5..c2bac662a 100644 --- a/app/controllers/notifications/trays_controller.rb +++ b/app/controllers/notifications/trays_controller.rb @@ -1,6 +1,6 @@ class Notifications::TraysController < ApplicationController def show - @notifications = Current.user.notifications.unread.ordered.limit(100) + @notifications = Current.user.notifications.preloaded.unread.ordered.limit(100) # Invalidate on the whole set instead of the unread set since the max updated at in the unread set # can stay the same when reading old notifications. diff --git a/app/models/card/assignable.rb b/app/models/card/assignable.rb index c43c50526..be0dd257e 100644 --- a/app/models/card/assignable.rb +++ b/app/models/card/assignable.rb @@ -15,7 +15,7 @@ module Card::Assignable end def assigned_to?(user) - assignments.exists? assignee: user + assignments.any? { |a| a.assignee_id == user.id } end def assigned? diff --git a/app/models/comment.rb b/app/models/comment.rb index a42b205fc..f81c465ad 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -4,11 +4,12 @@ class Comment < ApplicationRecord belongs_to :account, default: -> { card.account } belongs_to :card, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } - has_many :reactions, dependent: :delete_all + has_many :reactions, -> { order(:created_at) }, dependent: :delete_all has_rich_text :body scope :chronologically, -> { order created_at: :asc, id: :desc } + scope :preloaded, -> { with_rich_text_body.includes(reactions: :reacter) } scope :by_system, -> { joins(:creator).where(creator: { role: "system" }) } scope :by_user, -> { joins(:creator).where.not(creator: { role: "system" }) } diff --git a/app/models/notification.rb b/app/models/notification.rb index 416506783..545f6ab75 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -14,7 +14,7 @@ class Notification < ApplicationRecord after_destroy_commit :broadcast_read after_create :bundle - scope :preloaded, -> { preload(:creator, :account, source: [ :board, :creator ]) } + scope :preloaded, -> { preload(:creator, :account, source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ]) } delegate :notifiable_target, to: :source delegate :card, to: :source diff --git a/app/views/cards/_messages.html.erb b/app/views/cards/_messages.html.erb index c92c94f7c..402a39ec0 100644 --- a/app/views/cards/_messages.html.erb +++ b/app/views/cards/_messages.html.erb @@ -1,6 +1,6 @@ <%= messages_tag(card) do %> <% if card.published? %> - <%= render partial: "cards/comments/comment", collection: card.comments.chronologically, cached: true %> + <%= render partial: "cards/comments/comment", collection: card.comments.preloaded.chronologically, cached: true %> <%= render "cards/comments/new", card: card %> <%= render "cards/comments/watchers", card: card %> diff --git a/app/views/cards/comments/reactions/_reactions.html.erb b/app/views/cards/comments/reactions/_reactions.html.erb index 6de217842..ba3d491eb 100644 --- a/app/views/cards/comments/reactions/_reactions.html.erb +++ b/app/views/cards/comments/reactions/_reactions.html.erb @@ -1,7 +1,7 @@ <%= turbo_frame_tag comment, :reacting do %>
- <%= render partial: "cards/comments/reactions/reaction", collection: comment.reactions.includes(:reacter).ordered %> + <%= render partial: "cards/comments/reactions/reaction", collection: comment.reactions %>
<%= turbo_frame_tag comment, :new_reaction do %> diff --git a/test/controllers/cards/assignments_controller_test.rb b/test/controllers/cards/assignments_controller_test.rb index 9cfcc469f..c823174a8 100644 --- a/test/controllers/cards/assignments_controller_test.rb +++ b/test/controllers/cards/assignments_controller_test.rb @@ -11,12 +11,12 @@ class Cards::AssignmentsControllerTest < ActionDispatch::IntegrationTest end test "create" do - assert_changes "cards(:logo).assigned_to?(users(:david))", from: false, to: true do + assert_changes "cards(:logo).reload.assigned_to?(users(:david))", from: false, to: true do post card_assignments_path(cards(:logo)), params: { assignee_id: users(:david).id }, as: :turbo_stream assert_meta_replaced(cards(:logo)) end - assert_changes "cards(:logo).assigned_to?(users(:david))", from: true, to: false do + assert_changes "cards(:logo).reload.assigned_to?(users(:david))", from: true, to: false do post card_assignments_path(cards(:logo)), params: { assignee_id: users(:david).id }, as: :turbo_stream assert_meta_replaced(cards(:logo)) end diff --git a/test/models/card_test.rb b/test/models/card_test.rb index 4858e7203..e18ce43f8 100644 --- a/test/models/card_test.rb +++ b/test/models/card_test.rb @@ -37,7 +37,7 @@ class CardTest < ActiveSupport::TestCase assert_difference({ -> { cards(:logo).assignees.count } => -1, -> { Event.count } => +1 }) do cards(:logo).toggle_assignment users(:kevin) end - assert_not cards(:logo).assigned_to?(users(:kevin)) + assert_not cards(:logo).reload.assigned_to?(users(:kevin)) unassign_event = Event.last assert_equal "card_unassigned", unassign_event.action assert_equal [ users(:kevin) ], unassign_event.assignees From 0ab3aaca72a6315a8647bbdba855ff98f2659648 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 27 Nov 2025 11:27:10 -0500 Subject: [PATCH 19/24] Preload Event to avoid N+1s on the timeline --- app/models/event.rb | 10 ++++++++++ app/models/user/day_timeline.rb | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/models/event.rb b/app/models/event.rb index c759c18ee..92da7cf19 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -9,6 +9,16 @@ class Event < ApplicationRecord has_many :webhook_deliveries, class_name: "Webhook::Delivery", dependent: :delete_all scope :chronologically, -> { order created_at: :asc, id: :desc } + scope :preloaded, -> { + includes(:creator, :board, { + eventable: [ + :goldness, :closure, :image_attachment, + { rich_text_body: :embeds_attachments }, + { rich_text_description: :embeds_attachments }, + { card: [:goldness, :closure, :image_attachment] } + ] + }) + } after_create -> { eventable.event_was_created(self) } after_create_commit :dispatch_webhooks diff --git a/app/models/user/day_timeline.rb b/app/models/user/day_timeline.rb index b9ae4941a..1fe4ed36e 100644 --- a/app/models/user/day_timeline.rb +++ b/app/models/user/day_timeline.rb @@ -71,7 +71,7 @@ class User::DayTimeline def timelineable_events Event - .includes(:creator, :board, :eventable) + .preloaded .where(board: boards) .where(action: TIMELINEABLE_ACTIONS) end From 50fe9731051f2684a2af4b43fe509f7470a546a4 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 27 Nov 2025 11:30:54 -0500 Subject: [PATCH 20/24] Remove N+1 on the cards board. --- app/models/card.rb | 2 +- app/models/filter.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/card.rb b/app/models/card.rb index 33a993f71..0a589d0b7 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -22,7 +22,7 @@ class Card < ApplicationRecord scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order last_active_at: :desc, id: :desc } scope :with_users, -> { preload(creator: [ :avatar_attachment, :account ], assignees: [ :avatar_attachment, :account ]) } - scope :preloaded, -> { with_users.preload(:column, :tags, :steps, :closure, :goldness, :activity_spike, :image_attachment, board: [ :entropy ], not_now: [ :user ]).with_rich_text_description_and_embeds } + scope :preloaded, -> { with_users.preload(:column, :tags, :steps, :closure, :goldness, :activity_spike, :image_attachment, board: [ :entropy, :columns ], not_now: [ :user ]).with_rich_text_description_and_embeds } scope :indexed_by, ->(index) do case index diff --git a/app/models/filter.rb b/app/models/filter.rb index 7a398d03d..9dac977bd 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -18,7 +18,7 @@ class Filter < ApplicationRecord def cards @cards ||= begin - result = creator.accessible_cards.published + result = creator.accessible_cards.preloaded.published result = result.indexed_by(indexed_by) result = result.sorted_by(sorted_by) result = result.where(id: card_ids) if card_ids.present? From 198e404eb0657f4973a2d5baa37021180331fc32 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 27 Nov 2025 11:33:49 -0500 Subject: [PATCH 21/24] Avoid N+1 on account and board settings pages --- app/controllers/account/settings_controller.rb | 2 +- app/controllers/boards_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/account/settings_controller.rb b/app/controllers/account/settings_controller.rb index a076479cb..27f47ab99 100644 --- a/app/controllers/account/settings_controller.rb +++ b/app/controllers/account/settings_controller.rb @@ -3,7 +3,7 @@ class Account::SettingsController < ApplicationController before_action :set_account def show - @users = @account.users.active.alphabetically + @users = @account.users.active.alphabetically.includes(:identity) end def update diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 258bffc1a..1e4233696 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -24,7 +24,7 @@ class BoardsController < ApplicationController def edit selected_user_ids = @board.users.pluck :id @selected_users, @unselected_users = \ - @board.account.users.active.alphabetically.partition { |user| selected_user_ids.include? user.id } + @board.account.users.active.alphabetically.includes(:identity).partition { |user| selected_user_ids.include? user.id } end def update From e3d91f4ba27a1137a05626ba568089b3649f285d Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Thu, 27 Nov 2025 17:43:54 +0100 Subject: [PATCH 22/24] Fix join codes skipping user setup If someone joined an account with the same identity as they were signed in with the old logic would skip the user setup step --- app/controllers/join_codes_controller.rb | 5 +++- app/models/user.rb | 4 +++ .../controllers/join_codes_controller_test.rb | 18 +++++++++++++ test/models/identity/joinable_test.rb | 25 +++++++++++++++++++ test/models/user_test.rb | 10 ++++++++ 5 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/models/identity/joinable_test.rb diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb index 863522893..7e7ded229 100644 --- a/app/controllers/join_codes_controller.rb +++ b/app/controllers/join_codes_controller.rb @@ -13,9 +13,12 @@ class JoinCodesController < ApplicationController identity = Identity.find_or_create_by!(email_address: params.expect(:email_address)) @join_code.redeem { |account| identity.join(account) } unless identity.member_of?(@join_code.account) + user = User.active.find_by!(account: @join_code.account, identity: identity) - if identity == Current.identity + if identity == Current.identity && user.setup? redirect_to landing_url(script_name: @join_code.account.slug) + elsif identity == Current.identity + redirect_to new_users_join_url(script_name: @join_code.account.slug) else terminate_session if Current.identity diff --git a/app/models/user.rb b/app/models/user.rb index 976f1bdc0..90aab0338 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,4 +25,8 @@ class User < ApplicationRecord update! active: false, identity: nil end end + + def setup? + name != identity.email_address + end end diff --git a/test/controllers/join_codes_controller_test.rb b/test/controllers/join_codes_controller_test.rb index 9b386cb8f..74613c724 100644 --- a/test/controllers/join_codes_controller_test.rb +++ b/test/controllers/join_codes_controller_test.rb @@ -43,6 +43,9 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest identity = identities(:jz) sign_in_as :jz + assert identity.member_of?(@account), "JZ should be a member of 37s for this test" + assert identity.users.find_by!(account: @account).setup?, "JZ's user should be setup for this test" + assert_no_difference -> { Identity.count } do assert_no_difference -> { User.count } do post join_path(code: @join_code.code, script_name: @account.slug), params: { email_address: identity.email_address } @@ -51,4 +54,19 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest assert_redirected_to landing_url(script_name: @account.slug) end + + test "create for signed-in identity without a user in the account redirects to user setup" do + identity = identities(:mike) + sign_in_as :mike + + assert_not identity.member_of?(@account), "Mike should not be a member of 37s for this test" + + assert_no_difference -> { Identity.count } do + assert_difference -> { User.count }, 1 do + post join_path(code: @join_code.code, script_name: @account.slug), params: { email_address: identity.email_address } + end + end + + assert_redirected_to new_users_join_url(script_name: @account.slug) + end end diff --git a/test/models/identity/joinable_test.rb b/test/models/identity/joinable_test.rb new file mode 100644 index 000000000..8a8d40e1c --- /dev/null +++ b/test/models/identity/joinable_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class Identity::JoinableTest < ActiveSupport::TestCase + test "join" do + identity = identities(:david) + + user = identity.join(accounts(:initech)) + assert_kind_of User, user + assert_equal accounts(:initech), user.account + assert_equal identity.email_address, user.name + + identity = identities(:mike) + + user = identity.join(accounts("37s"), name: "Mike") + assert_kind_of User, user + assert_equal accounts("37s"), user.account + assert_equal "Mike", user.name + end + + test "member_of?" do + identity = identities(:david) + assert identity.member_of?(accounts("37s")) + assert_not identity.member_of?(accounts(:initech)) + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 6f5028bdb..5ffc5db56 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -35,4 +35,14 @@ class UserTest < ActiveSupport::TestCase assert_equal "DHH", User.new(name: "David Heinemeier Hansson").initials assert_equal "ÉLH", User.new(name: "Éva-Louise Hernández").initials end + + test "setup?" do + user = users(:kevin) + + user.update!(name: user.identity.email_address) + assert_not user.setup? + + user.update!(name: "Kevin") + assert user.setup? + end end From 0a0a8642c9ce33dc44bdada32f5dccf3c4a74a82 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 17:43:02 +0100 Subject: [PATCH 23/24] Fix: scrolling to parent navigable list despite of setting We need to check the setting of the parent controller https://app.fizzy.do/5986089/cards/3184 --- app/javascript/controllers/navigable_list_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/navigable_list_controller.js b/app/javascript/controllers/navigable_list_controller.js index f1d22e92d..4af794c4b 100644 --- a/app/javascript/controllers/navigable_list_controller.js +++ b/app/javascript/controllers/navigable_list_controller.js @@ -161,7 +161,7 @@ export default class extends Controller { #relayNavigationToParentNavigableList(event) { const parentController = this.#parentNavigableListController if (parentController) { - parentController.element.focus({ preventScroll: !this.autoScrollValue }) + parentController.element.focus({ preventScroll: !parentController.autoScrollValue }) parentController.navigate(event) } } From 09366fc94999bc0049a8675690be09dc2df8a5d2 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 27 Nov 2025 12:34:31 -0500 Subject: [PATCH 24/24] style: rubocop fix --- app/models/event.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/event.rb b/app/models/event.rb index 92da7cf19..bbbf2a60c 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -15,7 +15,7 @@ class Event < ApplicationRecord :goldness, :closure, :image_attachment, { rich_text_body: :embeds_attachments }, { rich_text_description: :embeds_attachments }, - { card: [:goldness, :closure, :image_attachment] } + { card: [ :goldness, :closure, :image_attachment ] } ] }) }