Skip to content

fix(sessions): keep sub-second precision in session start timestamps - #16834

Open
paulb777 wants to merge 2 commits into
mainfrom
pb-sessions-timestamp-precision
Open

paulb777 wants to merge 2 commits into
mainfrom
pb-sessions-timestamp-precision

Conversation

@paulb777

@paulb777 paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member

Problem

Time.timestampUS computed
Int64(UInt64(Date().timeIntervalSince1970) * USEC_PER_SEC). The UInt64
conversion drops the fractional second before the value is scaled. As a result,
the event_timestamp_us sent with every SessionStartEvent was rounded down to
a whole second, even though the field is in microseconds. This has been the case
since the file was added in #10311. Android sends
System.currentTimeMillis() * 1000, so it has millisecond precision. The same
UInt64 conversion would also trap if the device clock were ever set before
1970.

Change

  • Multiply the TimeInterval by USEC_PER_SEC before converting to Int64.
    This keeps sub-second precision and removes the UInt64 conversion. Any real
    Date is far inside the Int64 microsecond range (Date.distantFuture is
    about 6.4e16 µs).
  • Let Time take an injectable date source (currentTimeProvider, defaulting
    to Date.init, as in SessionInitiator) so the conversion can be unit tested.
    TimeProvider, MockTimeProvider, and the SessionStartEvent call site are
    unchanged.
  • Add TimeTests, plus a Crashlytics CHANGELOG entry, since Sessions has no
    changelog of its own.

Session timeout and rollover logic (SessionInitiator) uses Date directly and
is not affected.

Testing

mkdir -p .swiftpm/xcode/xcshareddata/xcschemes && cp scripts/spm_test_schemes/* .swiftpm/xcode/xcshareddata/xcschemes/
xcodebuild -scheme FirebaseSessionsUnit test -destination 'platform=macOS' -only-testing:FirebaseSessionsUnit/TimeTests/test_timestampUS_keepsFractionalSeconds
xcodebuild -scheme FirebaseSessionsUnit test -destination 'platform=macOS' -only-testing:FirebaseSessionsUnit/TimeTests/test_timestampUS_beforeUnixEpoch_isNegative
xcodebuild -scheme FirebaseSessionsUnit test -destination 'platform=macOS'
  • Before the fix (only the timestampUS arithmetic reverted, injection kept):
    • test_timestampUS_keepsFractionalSeconds fails with XCTAssertEqual failed: ("1700000000000000") is not equal to ("1700000000250000") and
      ... ("1700000000750000"). Two times in the same second collapse to the
      same value.
    • test_timestampUS_beforeUnixEpoch_isNegative crashes with Fatal error: Double value cannot be converted to UInt64 because the result would be less than UInt64.min.
  • After the fix: the whole FirebaseSessionsUnit target passes on macOS
    (66 tests, 0 failures), including both new tests and the existing
    SessionStartEventTests.
  • ./scripts/style.sh leaves the changed files unchanged.
    scripts/check_whitespace.sh and scripts/check_copyright.sh pass.

Fixes #16796

`Time.timestampUS` converted `Date().timeIntervalSince1970` to `UInt64`
before scaling it to microseconds, which dropped the fractional second.
Every `SessionStartEvent` therefore reported an `event_timestamp_us`
floored to a whole second, while the Android SDK reports millisecond
precision. The `UInt64` conversion would also trap if the device clock
were ever set before 1970.

Scale the `TimeInterval` before converting it to `Int64`, and let `Time`
take an injectable date source (defaulting to `Date.init`, like
`SessionInitiator`) so the conversion can be unit tested.

Fixes #16796
@paulb777 paulb777 added the sessions Changes pertaining to the Firebase Sessions SDK label Oct 2, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@paulb777

paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request resolves an issue where Firebase Sessions truncated session start event timestamps to whole seconds. It updates the Time class to retain fractional seconds when calculating the microsecond timestamp and introduces a customizable currentTimeProvider to facilitate testing. Additionally, unit tests have been added to verify fractional seconds and negative timestamps. The feedback suggests using .rounded() before casting the floating-point product to Int64 to prevent truncation errors due to floating-point precision limits.

Comment on lines 34 to 36
var timestampUS: Int64 {
return Int64(UInt64(Date().timeIntervalSince1970) * USEC_PER_SEC)
return Int64(currentTime().timeIntervalSince1970 * Double(USEC_PER_SEC))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

Converting a Double directly to Int64 performs truncation (rounding towards zero). Due to floating-point precision limits, the product of timeIntervalSince1970 and USEC_PER_SEC might be slightly less than the expected integer value (e.g., 1700000000000002.999999), which would truncate down to 1700000000000002 instead of rounding to 1700000000000003. Using .rounded() before casting to Int64 ensures the nearest microsecond is chosen and avoids systematic negative bias.

Suggested change
var timestampUS: Int64 {
return Int64(UInt64(Date().timeIntervalSince1970) * USEC_PER_SEC)
return Int64(currentTime().timeIntervalSince1970 * Double(USEC_PER_SEC))
}
var timestampUS: Int64 {
return Int64((currentTime().timeIntervalSince1970 * Double(USEC_PER_SEC)).rounded())
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[paulb777-bot] Thanks. Truncation is intentional. A timestamp normally floors to its unit, and this matches how other SDKs convert dates to microseconds, such as timeIntervalSince1970 * USEC_PER_SEC in Performance's FPRNanoPbUtils.m. The difference is at most 1 µs, which doesn't matter for session events.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: crashlytics sessions Changes pertaining to the Firebase Sessions SDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FirebaseSessions timestampUS discards fractional seconds

1 participant