Skip to content

fix(opentelemetry): Convert seconds timestamps in span.end() to milliseconds#19958

Open
logaretm wants to merge 1 commit intodevelopfrom
feat/fix-span-end-timestamp-conversion
Open

fix(opentelemetry): Convert seconds timestamps in span.end() to milliseconds#19958
logaretm wants to merge 1 commit intodevelopfrom
feat/fix-span-end-timestamp-conversion

Conversation

@logaretm
Copy link
Member

@logaretm logaretm commented Mar 24, 2026

Summary

  • Patches OTel span's end() method to run numeric timestamps through ensureTimestampInMilliseconds() before reaching OTel's native implementation
  • startTime already had this conversion, but span.end(timestamp) passed values directly to OTel which expects milliseconds — passing seconds (the Sentry convention) produced garbage timestamps
  • Applied in all three span creation paths: both code paths in _startSpan() and startInactiveSpan()

Closes #18697

…seconds

`startTime` was already converted via `ensureTimestampInMilliseconds()`, but
`span.end(timestamp)` passed the value directly to OTel which expects ms.
Passing seconds (the Sentry convention) produced garbage timestamps.

Patches the OTel span's `end()` method after creation so numeric timestamps
are normalized before reaching OTel's native implementation.

Closes #18697

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 24, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Deps

  • Bump mongodb-memory-server-global from 10.1.4 to 11.0.1 by dependabot in #19888
  • Bump stacktrace-parser from 0.1.10 to 0.1.11 by dependabot in #19887

Bug Fixes 🐛

Cloudflare

  • Send correct events in local development by JPeer264 in #19900
  • Forward ctx argument to Workflow.do user callback by Lms24 in #19891

Core

  • Preserve .withResponse() on Anthropic instrumentation by nicohrubec in #19935
  • Truncate content array format in Vercel by nicohrubec in #19911
  • Send internal_error as span status for Vercel error spans by nicohrubec in #19921
  • Do not overwrite user provided conversation id in Vercel by nicohrubec in #19903
  • Return same value from startSpan as callback returns by s1gr1d in #19300

Deps

  • Update lockfile to resolve [email protected] by chargome in #19933
  • Bump fast-xml-parser to 5.5.8 in @azure/core-xml chain by chargome in #19918
  • Bump next to 15.5.14 in nextjs-15 and nextjs-15-intl E2E test apps by chargome in #19917
  • Bump socket.io-parser to 4.2.6 to fix CVE-2026-33151 by chargome in #19880

Other

  • (craft) Add missing mainDocsUrl for @sentry/effect SDK by bc-sentry in #19860
  • (nestjs) Add node to nest metadata by chargome in #19875
  • (opentelemetry) Convert seconds timestamps in span.end() to milliseconds by logaretm in #19958
  • (serverless) Add node to metadata by nicohrubec in #19878

Internal Changes 🔧

Deps Dev

  • Bump effect from 3.19.19 to 3.20.0 by dependabot in #19926
  • Bump qunit-dom from 3.2.1 to 3.5.0 by dependabot in #19546
  • Bump @react-router/node from 7.13.0 to 7.13.1 by dependabot in #19544

Nuxt

  • Extract core logic for storage/database to prepare for Nuxt v5 by s1gr1d in #19920
  • Extract handler patching to extra plugin for Nitro v2/v3 by s1gr1d in #19915

Other

  • (astro) Re-enable server island tracing e2e test in Astro 6 by Lms24 in #19872
  • (ci) Fix "Gatbsy" typo in issue package label workflow by chargome in #19905
  • (claude) Enable Claude Code Intelligence (LSP) by s1gr1d in #19930
  • (lint) Resolve oxlint warnings by isaacs in #19893
  • (node-integration-tests) Remove unnecessary file-type dependency by Lms24 in #19824
  • (remix) Replace glob with native recursive fs walk by roli-lpci in #19531
  • (sveltekit) Replace recast + @babel/parser with acorn by roli-lpci in #19533
  • Add external contributor to CHANGELOG.md by javascript-sdk-gitflow in #19925
  • Add external contributor to CHANGELOG.md by javascript-sdk-gitflow in #19909

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.69 kB +0.2% +49 B 🔺
@sentry/browser - with treeshaking flags 24.17 kB +0.14% +33 B 🔺
@sentry/browser (incl. Tracing) 42.67 kB +0.13% +54 B 🔺
@sentry/browser (incl. Tracing, Profiling) 47.33 kB +0.12% +55 B 🔺
@sentry/browser (incl. Tracing, Replay) 81.48 kB +0.08% +57 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 71.06 kB +0.1% +69 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 86.17 kB +0.06% +50 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 98.41 kB +0.04% +36 B 🔺
@sentry/browser (incl. Feedback) 42.48 kB +0.08% +30 B 🔺
@sentry/browser (incl. sendFeedback) 30.35 kB +0.15% +43 B 🔺
@sentry/browser (incl. FeedbackAsync) 35.4 kB +0.12% +39 B 🔺
@sentry/browser (incl. Metrics) 26.96 kB +0.15% +38 B 🔺
@sentry/browser (incl. Logs) 27.1 kB +0.12% +32 B 🔺
@sentry/browser (incl. Metrics & Logs) 27.78 kB +0.15% +39 B 🔺
@sentry/react 27.45 kB +0.22% +58 B 🔺
@sentry/react (incl. Tracing) 45.01 kB +0.14% +60 B 🔺
@sentry/vue 30.13 kB +0.16% +46 B 🔺
@sentry/vue (incl. Tracing) 44.52 kB +0.09% +39 B 🔺
@sentry/svelte 25.7 kB +0.16% +40 B 🔺
CDN Bundle 28.35 kB +0.27% +75 B 🔺
CDN Bundle (incl. Tracing) 43.57 kB +0.15% +62 B 🔺
CDN Bundle (incl. Logs, Metrics) 29.22 kB +0.27% +77 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 44.43 kB +0.17% +75 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 68.29 kB +0.13% +85 B 🔺
CDN Bundle (incl. Tracing, Replay) 80.41 kB +0.1% +73 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 81.31 kB +0.1% +76 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 85.97 kB +0.12% +103 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 86.86 kB +0.1% +86 B 🔺
CDN Bundle - uncompressed 82.7 kB +0.1% +77 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 128.62 kB +0.05% +64 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 85.57 kB +0.1% +77 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 131.49 kB +0.05% +64 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 209.22 kB +0.05% +102 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 245.5 kB +0.04% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 248.35 kB +0.04% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 258.41 kB +0.04% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 261.26 kB +0.04% +89 B 🔺
@sentry/nextjs (client) 47.4 kB +0.08% +37 B 🔺
@sentry/sveltekit (client) 43.12 kB +0.12% +51 B 🔺
@sentry/node-core 56.46 kB +0.2% +112 B 🔺
@sentry/node 173.58 kB +0.25% +426 B 🔺
@sentry/node - without tracing 96.47 kB +0.13% +121 B 🔺
@sentry/aws-serverless 113.48 kB +0.13% +142 B 🔺

View base workflow run

@github-actions
Copy link
Contributor

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,858 - 8,837 +0%
GET With Sentry 1,543 17% 1,581 -2%
GET With Sentry (error only) 5,830 66% 5,836 -0%
POST Baseline 1,158 - 1,170 -1%
POST With Sentry 516 45% 561 -8%
POST With Sentry (error only) 998 86% 1,019 -2%
MYSQL Baseline 3,104 - 3,195 -3%
MYSQL With Sentry 295 10% 406 -27%
MYSQL With Sentry (error only) 2,507 81% 2,586 -3%

View base workflow run

@logaretm logaretm requested a review from timfish March 24, 2026 18:20
@logaretm logaretm marked this pull request as ready for review March 24, 2026 18:20
@logaretm logaretm requested review from Lms24, Copilot and s1gr1d March 24, 2026 18:20
Copy link
Contributor

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

This PR fixes a timestamp unit mismatch in @sentry/opentelemetry by ensuring numeric timestamps passed to span.end(timestamp) are converted from seconds (Sentry convention) to milliseconds (OTel expectation), matching existing startTime behavior.

Changes:

  • Patch OTel spans’ end() method to run numeric endTime values through ensureTimestampInMilliseconds() before delegating to the native OTel implementation.
  • Apply the end() patch across the span creation pathways in _startSpan() and startInactiveSpan().
  • Add unit tests covering span.end() with number (seconds/ms), Date, HrTime, and no-arg usage.

Reviewed changes

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

File Description
packages/opentelemetry/src/trace.ts Wraps Span.end() to convert numeric seconds timestamps to milliseconds before calling OTel.
packages/opentelemetry/test/trace.test.ts Adds coverage validating end-time conversion behavior for multiple TimeInput forms.

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

Comment on lines 204 to 206
function ensureTimestampInMilliseconds(timestamp: number): number {
const isMs = timestamp < 9999999999;
return isMs ? timestamp * 1000 : timestamp;
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

ensureTimestampInMilliseconds uses const isMs = timestamp < 9999999999, but that condition is actually detecting seconds (10-digit epoch) rather than milliseconds. The current variable name is misleading and makes it easy to accidentally invert the logic later. Rename the boolean (and/or extract a named constant for the threshold) to reflect that it’s checking for seconds so the intent stays clear.

Copilot uses AI. Check for mistakes.
Comment on lines +2018 to +2022
// Use a timestamp in seconds that is after the span start (i.e. in the future)
// OTel resets endTime to startTime if endTime < startTime
const nowSec = Math.floor(Date.now() / 1000) + 1;
const span = startInactiveSpan({ name: 'test' });
span.end(nowSec);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

These tests use Math.floor(Date.now() / 1000) + 1 to ensure the end timestamp is after the span start time. With only a 1s buffer, slow CI (or a delay crossing a second boundary) can still produce endTime < startTime, causing OTel to clamp/reset the end time and making the assertions flaky. Consider deriving the end timestamp from the span’s actual startTime (or using a larger, deterministic offset) so the end time is guaranteed to be >= start time.

Copilot uses AI. Check for mistakes.
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.

Node.js: span.end(timestamp) requires millisecond epoch

2 participants