Skip to content

fix(daily-events): temporarly convert invalid timesamps - #680

Merged
neSpecc merged 4 commits into
masterfrom
fix-sentry-sentat
Sep 15, 2026
Merged

neSpecc merged 4 commits into
masterfrom
fix-sentry-sentat

Conversation

@neSpecc

@neSpecc neSpecc commented Sep 15, 2026

Copy link
Copy Markdown
Member
  • dailyEventsPortion failed on projects with legacy far-future Sentry timestamps (~2056) stored in Mongo: values exceeded GraphQL Int (32-bit) on groupingTimestamp / cursor boundaries.
  • Add temporary on-read sanitization: clamp Int-unsafe timestamps (prefer ObjectId receive time) and counts before serialization; keep schema as Int.
  • Helpers marked for removal after ~2026-11-15 once collector clamp ages out bad docs.

Fixes:

GraphQLError: Int cannot represent non 32-bit signed integer value: 2736187957
    at GraphQLScalarType.serialize (/Users/specc/codex/hawk.mono/api/node_modules/graphql/type/scalars.js:69:13)
    at completeLeafValue (/Users/specc/codex/hawk.mono/api/node_modules/graphql/execution/execute.js:738:39)
    at completeValue (/Users/specc/codex/hawk.mono/api/node_modules/graphql/execution/execute.js:619:12)
    at completeValue (/Users/specc/codex/hawk.mono/api/node_modules/graphql/execution/execute.js:584:23)
    at /Users/specc/codex/hawk.mono/api/node_modules/graphql/execution/execute.js:486:9
    at processTicksAndRejections (node:internal/process/task_queues:103:5)
    at async Promise.all (index 1) {
  path: undefined,
  locations: undefined,
  extensions: [Object: null prototype] {}
}

Copilot AI 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.

🟡 Changes recommended

Unresolved cursor pagination and timestamp/count sanitization issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR temporarily sanitizes legacy timestamps and oversized counts in dailyEventsPortion while retaining the GraphQL Int schema.

Changes:

  • Adds GraphQL-safe timestamp, count, ObjectId, and cursor helpers.
  • Sanitizes daily-event responses and pagination cursors.
  • Adds regression tests and bumps the version to 1.5.15.
File summaries
File Summary
test/utils/graphqlIntSafe.test.ts Adds helper coverage.
test/resolvers/project-daily-events-portion.test.ts Adds resolver regression coverage.
src/utils/graphqlIntSafe.js Moderate (2 votes): Millisecond normalization can still be reported as safe. Moderate (3 votes): Count-based cursor boundaries may be misclassified as timestamps.
src/resolvers/project.js Critical (3 votes): Converted cursors can become incomparable with raw Mongo values and skip rows. Moderate (1 vote): Raw millisecond repetition times can produce overflowing grouping timestamps. Moderate (1 vote): Sanitizing timestamp alone can make it inconsistent with originalTimestamp and alter valid Float values.
package.json Bumps the version to 1.5.15.
Review details

Suppressed comments (2)

src/resolvers/project.js:157

  • Event.timestamp is declared as Float!, so it does not need 32-bit Int sanitization. For an event without a repetition, the factory sets originalTimestamp equal to timestamp; rewriting only timestamp here leaves the response internally inconsistent, and also changes valid Float timestamps outside the helper's 10-year window. Restrict this conversion to Int-backed fields or sanitize both timestamp fields consistently.
    const safeEventTimestamp = toSafeUnixTimestampForGraphQLInt(
      event.timestamp,
      fallbackId,
      nowSec
    );

src/resolvers/project.js:133

  • When lastRepetitionTime is stored in milliseconds, isUnsafeUnixTimestamp normalizes it and marks it safe, but this branch passes the raw millisecond value to utcMidnightUnix. That produces a grouping timestamp around 1e12 seconds, which is still outside GraphQL Int and can reproduce the serialization failure. Use safeLastRepetitionTime for the numeric branch so the grouping calculation uses the normalized seconds value.
        : (typeof dailyEvent.lastRepetitionTime === 'number'
          ? dailyEvent.lastRepetitionTime
          : safeGroupingTimestamp)
  • Files reviewed: 5/5 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/resolvers/project.js
Comment thread src/utils/graphqlIntSafe.js Outdated
Comment thread src/utils/graphqlIntSafe.js Outdated

Copilot AI 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.

🔵 Needs a closer look

Moderate timestamp and pagination issues remain, along with resolver coverage gaps.

Review details

Suppressed comments (9)

Previously missed (1) — in code that hasn't changed since the last review.

src/utils/graphqlIntSafe.js:145

  • The sanitizer returns NaN/Infinity unchanged. Those values can still reach DailyEvent.groupingTimestamp or event.timestamp and make GraphQL serialization fail, so non-finite numeric timestamps need the same ObjectId/current-time fallback as out-of-range timestamps.

src/resolvers/project.js:159

  • event.timestamp is declared as GraphQL Float!, not Int!, but this helper always truncates it through normalizeUnixSeconds. A valid fractional-seconds timestamp such as 1700000000.5 is therefore changed to 1700000000 even though it is representable by the schema; the same path also applies the 10-year/future fallback policy to a field that does not have the reported Int overflow. Restrict this conversion to the Int-backed fields, or preserve Float timestamps unless a separate, explicit millisecond-normalization contract is required.
    const safeEventTimestamp = toSafeUnixTimestampForGraphQLInt(
      event.timestamp,
      fallbackId,
      nowSec
    );

src/resolvers/project.js:86

  • Converting the cursor to ObjectId time (or Int max) changes the boundary that findDailyEventsPortion uses against the raw Mongo fields. If a page ends on a legacy row, the next request will compare groupingTimestamp/the sort field to the sanitized value, so remaining legacy rows with their original far-future timestamp or oversized count are filtered out and silently disappear from pagination. Preserve a raw continuation token or update the factory query to account for the sanitized boundary instead of returning a cursor with different ordering semantics.
  return {
    ...cursor,
    groupingTimestampBoundary: safeGrouping,
    sortValueBoundary: safeSort,

src/resolvers/project.js:143

  • The resolver-level tests only use ordinary count/affectedUsers values, so this new daily-event sanitization path is not covered end to end. Add a fixture with values above GRAPHQL_INT_MAX and assert the resolver returns capped fields; otherwise a regression here can still surface the original GraphQL Int failure despite the utility test.
  const safeCount = typeof (dailyEvent && dailyEvent.count) === 'number'
    ? toSafeGraphQLInt(dailyEvent.count, 0)
    : dailyEvent.count;
  const safeAffectedUsers = typeof (dailyEvent && dailyEvent.affectedUsers) === 'number'
    ? toSafeGraphQLInt(dailyEvent.affectedUsers, 0)

src/resolvers/project.js:154

  • The nested event count sanitization is likewise only exercised with normal values in the new resolver test (totalCount: 13692, usersAffected: 0). Add an end-to-end case for oversized totalCount/usersAffected so the GraphQL-facing mapping is protected, not just toSafeGraphQLInt in isolation.
    const safeTotalCount = typeof event.totalCount === 'number'
      ? toSafeGraphQLInt(event.totalCount, 0)
      : event.totalCount;
    const safeUsersAffected = typeof event.usersAffected === 'number'
      ? toSafeGraphQLInt(event.usersAffected, 0)
      : event.usersAffected;

src/utils/graphqlIntSafe.js:18

  • This makes every timestamp older than nowSec - MAX_PAST_SEC unsafe even when it is well within GraphQL's signed 32-bit range. dailyEventsPortion has no date-retention filter, so a legitimate historical event (for example, from 2015) will have its lastRepetitionTime, event timestamp, and possibly day bucket rewritten to the ObjectId receive time. Please restrict this fallback to the actual overflow/invalid-future cases, or establish and enforce an explicit retention guarantee before applying this 10-year cutoff.
const MAX_PAST_SEC = 10 * 365.25 * 24 * 60 * 60;

src/utils/graphqlIntSafe.js:43

  • NaN and Infinity are not representable by GraphQL Int, but this branch reports both as in-range. Any caller relying on this predicate can therefore pass a non-finite value through to serialization; treat non-finite numbers as out of range.
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return false;
  }

src/utils/graphqlIntSafe.js:205

  • Cursor boundaries take this early return before reaching either toSafeUnixTimestampForGraphQLInt or toSafeGraphQLInt, so a non-finite sortValueBoundary is emitted unchanged and the Int! cursor field still throws during GraphQL serialization. Only bypass non-number values here; let non-finite numbers be normalized.
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return value;
  }

src/utils/graphqlIntSafe.js:171

  • isUnsafeUnixTimestamp also classifies NaN/Infinity as safe. In sanitizeDailyEvent, that leaves groupingNeedsFix false, so a non-finite grouping timestamp is replaced with the fallback instant rather than being normalized to a UTC day boundary.
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return false;
  }
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@neSpecc
neSpecc merged commit 2c47c17 into master Sep 15, 2026
8 checks passed
@neSpecc
neSpecc deleted the fix-sentry-sentat branch September 15, 2026 20:08
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