Skip to content

fix: ParseInstallation saves timeZone field as empty string#1141

Open
AdrianCurtin wants to merge 4 commits into
parse-community:masterfrom
AdrianCurtin:tz_fix
Open

fix: ParseInstallation saves timeZone field as empty string#1141
AdrianCurtin wants to merge 4 commits into
parse-community:masterfrom
AdrianCurtin:tz_fix

Conversation

@AdrianCurtin
Copy link
Copy Markdown

@AdrianCurtin AdrianCurtin commented May 22, 2026

Pull Request

Issue

N/A — bug surfaced during local testing; no existing issue.

Approach

ParseInstallation._getNameLocalTimeZone() returned "" instead of an IANA timezone name (e.g. America/New_York), so installations were persisted with an empty timeZone field. This affects any server-side logic that targets users by locale, including push scheduling.

Root cause:

  • The method compared DateTime.now().timeZoneOffset (a Duration) against TimeZone.offset from the timezone package. On timezone <0.11.0 that field is an int (ms), so the equality check is always false and name stayed "". On timezone >=0.11.0 both sides are Duration, but the loop scanned every historical zone for each location (LMT, pre-DST, etc.) and the outer forEach never broke out, so whichever location came last in map iteration won.

Fix:

  • Capture DateTime.now() once to avoid a DST-transition race between the two reads.
  • Prefer DateTime.now().timeZoneName when it's a valid IANA location key (macOS, Linux, iOS, Android typically report IANA names directly).
  • Fall back to matching location.currentTimeZone (the current zone, not the full historical list) against the local offset. Handles both int and Duration offset shapes so the SDK stays compatible across the full timezone: ">=0.9.4 <0.12.0" constraint.
  • Last resort returns the OS-reported name instead of "". On Windows/Web this may be non-IANA ("Pacific Standard Time", "EDT"), but it's strictly better than empty.

Tests:
Expanded parse_installation_test.dart from one weak containsKey assertion (which passed even with the "" bug) to four cases:

  • timeZone field exists
  • regression: timeZone is non-empty
  • timeZone is either an IANA name or the OS-reported fallback
  • when IANA-resolved, its current offset equals the system offset

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)

Summary by CodeRabbit

  • Bug Fixes
    • Preserve caller-provided installation timeZone instead of overwriting it.
    • Improve timezone detection and initialize timezone data only once to avoid DST-related inconsistencies.
    • Prefer a valid IANA zone when available; otherwise fall back to the OS-reported timezone name.
    • Ensure stored timezone is non-empty and matches the system offset for consistent cross-environment reporting.

Review Change Stack

Improve local timezone name resolution by capturing DateTime.now() once to avoid DST race, preferring the OS-reported IANA zone when available, and falling back to matching a location whose current zone offset equals the local offset. This also fixes a bug where the old code compared incompatible types (int vs Duration) and could produce an empty string. Tests updated to initialize Parse/timezone data in setUpAll and to assert the installation's timeZone field exists, is non-empty, is either an IANA name or the OS-reported name, and (when matched via offset) has an offset equal to the system offset.
Copilot AI review requested due to automatic review settings May 22, 2026 03:40
@parse-github-assistant
Copy link
Copy Markdown

parse-github-assistant Bot commented May 22, 2026

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement. Our CI and AI review are safeguards, not development tools. If many issues are flagged, rethink your development approach. Invest more effort in planning and design rather than using review cycles to fix low-quality code.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d93bde9b-2ae6-49aa-96c8-8987c17d0d6e

📥 Commits

Reviewing files that changed from the base of the PR and between 44ff19b and 823c3d4.

📒 Files selected for processing (1)
  • packages/dart/lib/src/objects/parse_installation.dart

📝 Walkthrough

Walkthrough

The PR rewrites local timezone resolution to initialize timezone data once, capture the current moment once, prefer OS timezone names that match IANA locations, match zones by normalized offset when needed, and fall back to the OS name; tests are refactored and expanded to validate presence, non-emptiness, IANA/fallback validity, and offset correctness.

Changes

Timezone Name Resolution and Test Suite

Layer / File(s) Summary
Timezone resolution implementation
packages/dart/lib/src/objects/parse_installation.dart
Adds a process-scoped _timeZonesInitialized guard; _updateInstallation() preserves an existing timeZone; rewrites _getNameLocalTimeZone() to initialize tz data once, capture DateTime.now() once, prefer IANA-valid timeZoneName, match by normalized offset via _zoneOffsetMs, and fall back to the OS timeZoneName.
Test refactor and regression checks
packages/dart/test/parse_installation_test.dart
Adds timezone imports and a shared _initParse() helper, initializes timezone DB once in setUpAll, clears persisted installation before each test, and replaces the single existence check with tests for key presence, non-empty stored value, IANA-or-OS fallback validity, and offset equality when the location exists.

Sequence Diagram

sequenceDiagram
  participant ParseInstallation
  participant TimezoneDB
  participant OSClock
  ParseInstallation->>OSClock: capture now = DateTime.now()
  ParseInstallation->>TimezoneDB: ensure tz.initializeTimeZones() (once, guard _timeZonesInitialized)
  ParseInstallation->>TimezoneDB: lookup OS timeZoneName as IANA location
  alt found IANA location
    TimezoneDB-->>ParseInstallation: return location (may include offset)
  else not found
    TimezoneDB-->>ParseInstallation: search zones by current offset (use _zoneOffsetMs)
  end
  ParseInstallation-->>OSClock: return resolved timeZoneName (IANA or OS name)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Engage In Review Feedback ❓ Inconclusive Review feedback engagement verification requires access to GitHub PR comments and discussions, which are not accessible in the sandboxed environment where only git commit history is available. To verify this check, access the actual GitHub PR #1141 interface at github.com to review: (1) review comments from maintainers, (2) author responses/discussions on those comments, and (3) commits addressing feedback.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title begins with 'fix:' which is one of the required prefixes and accurately summarizes the bug being fixed.
Description check ✅ Passed The description follows the template structure with all required sections (Issue, Approach, Tasks) completed with substantial detail about the bug, root causes, fixes, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed No security vulnerabilities introduced. Code uses trusted APIs, has no injection/RCE patterns, proper type handling, and resource management with initialization guards.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR improves how ParseInstallation derives and persists the local timeZone, and adds regression tests to prevent it from being stored as an empty string due to mismatched offset types in older timezone versions.

Changes:

  • Refactors _getNameLocalTimeZone() to prefer OS-reported IANA zone names and fixes offset matching across timezone versions.
  • Adds tests asserting the timeZone field exists, is non-empty, and is either an IANA zone or the OS-reported name.
  • Adds a regression test verifying offset-based matches correspond to the local offset.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
packages/dart/lib/src/objects/parse_installation.dart Fixes and hardens local timezone name detection (IANA preference + offset fallback).
packages/dart/test/parse_installation_test.dart Adds regression/behavior tests validating non-empty and valid timezone persistence.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +97 to +100
tz.initializeTimeZones();
var locations = tz.timeZoneDatabase.locations;

Duration offset = DateTime.now().timeZoneOffset;
String name = "";
// Capture once to avoid a DST-transition race between the two reads.
final DateTime now = DateTime.now();
Comment on lines +61 to +76
tz.initializeTimeZones();
final installation = await ParseInstallation.currentInstallation();
final tzValue = installation.get<String>(keyTimeZone)!;

// act
final ParseInstallation installation =
await ParseInstallation.currentInstallation();
final location = tz.timeZoneDatabase.locations[tzValue];
if (location == null) {
// OS-reported, non-IANA fallback (Windows/Web). Nothing to verify.
return;
}

dynamic actualHasTimeZoneResult = installation.containsKey(keyTimeZone);
final dynamic zoneOffset = location.currentTimeZone.offset;
final int zoneOffsetMs = zoneOffset is Duration
? zoneOffset.inMilliseconds
: zoneOffset as int;

// assert
expect(actualHasTimeZoneResult, true);
expect(zoneOffsetMs, equals(DateTime.now().timeZoneOffset.inMilliseconds));

test('installation timeZone is an IANA name or the OS-reported name',
() async {
tz.initializeTimeZones();

test('when timeZone is matched via offset, its offset equals the local offset',
() async {
tz.initializeTimeZones();
Comment on lines +116 to +119
final dynamic zoneOffset = location.currentTimeZone.offset;
final int zoneOffsetMs = zoneOffset is Duration
? zoneOffset.inMilliseconds
: zoneOffset as int;
@AdrianCurtin AdrianCurtin changed the title Fix timezone detection in ParseInstallation fix: ParseInstallation saves timeZone field as empty string May 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dart/test/parse_installation_test.dart`:
- Around line 41-76: The tests read a persisted ParseInstallation (via
ParseInstallation.currentInstallation) and compare its keyTimeZone against
DateTime.now(), which can flake across DST changes; before each test recreate or
clear the persisted installation by removing the stored key
(keyParseStoreInstallation) or resetting the in-memory singleton/state (e.g.
_currentInstallationId) so currentInstallation() returns a fresh installation,
then set/get keyTimeZone for the new instance and run the assertions; update
test setup/tearDown to clear persistence and reset the internal current
installation state so comparisons to DateTime.now() are always against a
freshly-created installation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e016ac40-6fc7-4153-b599-077d22f68c56

📥 Commits

Reviewing files that changed from the base of the PR and between 785f76b and 8c02135.

📒 Files selected for processing (2)
  • packages/dart/lib/src/objects/parse_installation.dart
  • packages/dart/test/parse_installation_test.dart

Comment thread packages/dart/test/parse_installation_test.dart Outdated
Avoid repeatedly loading the timezone database by adding a process-wide _timeZonesInitialized flag and initializing tz data at most once in ParseInstallation. Add a helper _zoneOffsetMs to normalize TimeZone.offset (handles both int milliseconds on older timezone package versions and Duration on newer versions). Update tests to initialize timezones in setUpAll, clear persisted installation between tests to avoid stale data across DST boundaries, and capture DateTime.now() once per test to prevent racey comparisons.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dart/lib/src/objects/parse_installation.dart`:
- Around line 119-123: The fallback in _getNameLocalTimeZone currently returns
the first tz.timeZoneDatabase location whose
_zoneOffsetMs(location.currentTimeZone.offset) equals localOffsetMs, which can
pick an arbitrary IANA name when multiple zones share the same offset; change it
to collect all matching locations (using tz.timeZoneDatabase.locations.values
and _zoneOffsetMs) and only return the matching location.name if there is
exactly one match, otherwise return the original OS-reported system name
(now.timeZoneName) so we don't persist an arbitrary IANA zone.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2281b152-1e9f-444f-828c-e1f2f8b9c389

📥 Commits

Reviewing files that changed from the base of the PR and between 8c02135 and 1da0147.

📒 Files selected for processing (2)
  • packages/dart/lib/src/objects/parse_installation.dart
  • packages/dart/test/parse_installation_test.dart

Comment thread packages/dart/lib/src/objects/parse_installation.dart
Avoid overwriting a caller-provided timeZone on ParseInstallation. The change checks the existing value (via super.get) and only fills in the offset-matched local IANA name when the field is null or empty. Added comments explaining the offset-match fallback (which may pick an alphabetical IANA zone rather than a regionally accurate one), recommends apps set a real IANA name when available, and documents a first-launch caveat where the fallback can be persisted before caller code runs.
Change timezone fallback logic to only return an IANA zone if exactly one location matches the local offset. Many zones share the same instant offset (e.g. America/Los_Angeles and America/Vancouver), so returning the first match could fabricate a wrong location. The code now records a single match and clears it if a second match is found, falling back to the OS string when ambiguous. Also centralizes the local offset calculation and fixes comparisons against historical zone offsets.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants