From ee1ea9576b37a28bab8fd4a25ae3092745c83a68 Mon Sep 17 00:00:00 2001 From: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:26:04 +0530 Subject: [PATCH] fix: Timezone offset reports broken by DST transition (#12747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fixes timezone offset parameter in V2 reports API that was broken by DST transitions. The issue occurred when UK DST ended on October 26, 2025, causing the test to fail starting October 27th. ~~**Initial diagnosis:** The root cause was that `timezone_name_from_offset` used `zone.now.utc_offset` to match timezones, which changes based on the current date's DST status rather than the data being queried.~~ **Actual root cause:** The test was accidentally passing before DST transition. During BST, `timezone_name_from_offset(0)` matched "Azores" (UTC-1) instead of "Edinburgh" (UTC+0), and the -1 hour offset coincidentally split midnight data into [1,5]. After DST ended, it correctly matched "Edinburgh" (UTC+0), but this grouped all conversations into one day [6], exposing that the test data was flawed. The real issue: Test data created all 6 conversations starting at midnight on a single day, which cannot produce a [1,5] split in true UTC. Fixes CW-5846 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? **Test that was failing:** ```bash bundle exec rspec spec/controllers/api/v2/accounts/reports_controller_spec.rb:25 ``` **Changes:** ~~1. Fixed `timezone_name_from_offset` to use January 1st as reference date instead of current date~~ ~~2. Converted timezone string to `ActiveSupport::TimeZone` object for `group_by_period` compatibility~~ **Revised approach:** 1. Freeze test time to January 2024 using `travel_to`, making timezone matching deterministic and aligned with test data period 2. Start test conversations at 23:00 instead of midnight to properly span two days and test timezone boundary grouping 3. Keep `zone.now.utc_offset` (correct behavior for real users during DST) **Why this works:** - Test runs "in January 2024" → `zone.now.utc_offset` returns January offsets consistently - Offset `-8` correctly matches Pacific Standard Time (UTC-8 in January) - Real users in PDT (summer) with offset `-7` → correctly match Pacific Daylight Time - No production impact, test is deterministic year-round **Verification:** - Test now passes consistently regardless of current DST status - Timezone matching works correctly for real users during DST periods - Reports correctly group data by timezone offset across all seasons ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Shivam Mishra Co-authored-by: Muhsin Keloth --- .rubocop.yml | 4 +- .../v2/accounts/reports_controller_spec.rb | 60 ++++++++++--------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index e30a71ee9..ea688792b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,7 +23,7 @@ Metrics/MethodLength: - 'enterprise/lib/captain/agent.rb' RSpec/ExampleLength: - Max: 25 + Max: 50 Style/Documentation: Enabled: false @@ -336,4 +336,4 @@ FactoryBot/RedundantFactoryOption: Enabled: false FactoryBot/FactoryAssociationWithStrategy: - Enabled: false \ No newline at end of file + Enabled: false diff --git a/spec/controllers/api/v2/accounts/reports_controller_spec.rb b/spec/controllers/api/v2/accounts/reports_controller_spec.rb index 4dbbcf406..2d505822f 100644 --- a/spec/controllers/api/v2/accounts/reports_controller_spec.rb +++ b/spec/controllers/api/v2/accounts/reports_controller_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do context 'when authenticated and authorized' do before do # Create conversations across 24 hours at different times - base_time = Time.utc(2024, 1, 15, 0, 0) # Start at midnight UTC + base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days # Create conversations every 4 hours across 24 hours 6.times do |i| @@ -23,36 +23,38 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do end it 'timezone_offset affects data grouping and timestamps correctly' do - Time.use_zone('UTC') do - base_time = Time.utc(2024, 1, 15, 0, 0) - base_params = { - metric: 'conversations_count', - type: 'account', - since: (base_time - 1.day).to_i.to_s, - until: (base_time + 2.days).to_i.to_s, - group_by: 'day' - } + travel_to Time.utc(2024, 1, 15, 12, 0) do + Time.use_zone('UTC') do + base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days + base_params = { + metric: 'conversations_count', + type: 'account', + since: (base_time - 1.day).to_i.to_s, + until: (base_time + 2.days).to_i.to_s, + group_by: 'day' + } - responses = [0, -8, 9].map do |offset| - get "/api/v2/accounts/#{account.id}/reports", - params: base_params.merge(timezone_offset: offset), - headers: admin.create_new_auth_token, as: :json - response.parsed_body + responses = [0, -8, 9].map do |offset| + get "/api/v2/accounts/#{account.id}/reports", + params: base_params.merge(timezone_offset: offset), + headers: admin.create_new_auth_token, as: :json + response.parsed_body + end + + data_entries = responses.map { |r| r.select { |e| e['value'] > 0 } } + totals = responses.map { |r| r.sum { |e| e['value'] } } + timestamps = responses.map { |r| r.map { |e| e['timestamp'] } } + + # Data conservation and redistribution + expect(totals.uniq).to eq([6]) + expect(data_entries[0].map { |e| e['value'] }).to eq([1, 5]) + expect(data_entries[1].map { |e| e['value'] }).to eq([3, 3]) + expect(data_entries[2].map { |e| e['value'] }).to eq([4, 2]) + + # Timestamp differences + expect(timestamps.uniq.size).to eq(3) + timestamps[0].zip(timestamps[1]).each { |utc, pst| expect(utc - pst).to eq(-28_800) } end - - data_entries = responses.map { |r| r.select { |e| e['value'] > 0 } } - totals = responses.map { |r| r.sum { |e| e['value'] } } - timestamps = responses.map { |r| r.map { |e| e['timestamp'] } } - - # Data conservation and redistribution - expect(totals.uniq).to eq([6]) - expect(data_entries[0].map { |e| e['value'] }).to eq([1, 5]) - expect(data_entries[1].map { |e| e['value'] }).to eq([3, 3]) - expect(data_entries[2].map { |e| e['value'] }).to eq([4, 2]) - - # Timestamp differences - expect(timestamps.uniq.size).to eq(3) - timestamps[0].zip(timestamps[1]).each { |utc, pst| expect(utc - pst).to eq(-28_800) } end end