Skip to content

fix(flags): stop month lookbacks overflowing into the next month - #5626

Merged
marandaneto merged 2 commits into
PostHog:mainfrom
luantaraschi:fix/relative-date-month-overflow
Nov 10, 2026
Merged

marandaneto merged 2 commits into
PostHog:mainfrom
luantaraschi:fix/relative-date-month-overflow

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Problem

relativeDateParseForFeatureFlagMatching subtracts months with setUTCMonth, which rolls an impossible date forward. A 1m filter evaluated on March 31 resolves to March 3 instead of February 28, and 1y on February 29 resolves to March 1. posthog-node and the Convex component use this parser for local evaluation, so is_date_before and is_date_after can answer differently from the flags service on those days.

Changes

Months and years now subtract one unit at a time and clamp the day after every step, as the local evaluator spec requires, so a two-month lookback from March 31 gives January 28. The SDK still works in UTC where the spec asks for the team's timezone, and I left that alone.

Eight of the twelve new table cases fail on main, and so does a new test that goes through matchFeatureFlagProperty with is_date_after. I also compared the two helpers against a naive month-by-month loop for every day from 2019 through 2028, with lookbacks from 0 to 129 plus 4000 and 9999, and got no differences in 964,392 comparisons.

I ran this in node:24-slim: vitest in packages/core (2368 passed, 2 skipped) and packages/node (1516 passed), the flag specs in packages/convex (83 passed), oxfmt --check on the changed files, and oxlint on packages/core/src. turbo run build --filter=posthog-node also passes.

Release info Sub-libraries affected

Libraries affected

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react-native-plugin
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/openfeature-node-provider
  • @posthog/openfeature-web-provider
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types
  • @posthog/browser-common

Checklist

  • All commits are signed (see Commit signing)
  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Added a changeset file (written by hand, I didn't run pnpm changeset)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Luan Taraschi directed this and Claude Code (Claude Opus 5.5) wrote it and ran the checks. It came out of the same spec comparison as PostHog/posthog-python#1212, which fixes the single-jump version of this bug in the Python SDK.

setUTCMonth rolls an impossible date forward, so 1m on March 31 resolved to March 3. Subtract one unit at a time and clamp the day after every step, as the SDK spec requires.
@luantaraschi
luantaraschi requested a review from a team as a code owner November 9, 2026 04:59
The table tests call the parser directly. This one goes through matchFeatureFlagProperty on the last day of March.

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct.

@github-actions

Copy link
Copy Markdown
Contributor

SDK performance: posthog-node

1 change worth reviewing: 1 observed regression.

Flags require at least a 5% change in the median and all six pairs agreeing on direction. This is a noise filter, not proof of a performance change or a CI gate.

Test Measurement Before PR With PR Change
Evaluate all 1,000 flags Time per operation 16.67 ms/op 17.69 ms/op 6.1% more time
View all results
Test Measurement Before PR With PR Change Assessment
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Queue an event (32-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Queue an event (4,096-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate all 10 flags
Shard: flags-scaling
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate all 100 flags
Shard: flags-scaling
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate all 1,000 flags
Shard: flags-scaling
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate one boolean flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a cohort-filtered flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a flag (early match)
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a flag (late match)
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a flag (no match)
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a property-filtered flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a regex-filtered flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a rollout flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a multivariate flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocated memory per operation (estimated) 10.2 KiB/op 9.83 KiB/op 3.7% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Time per operation 12.41 µs/op 12.13 µs/op 2.3% less time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Idle process memory (RSS) 102.64 MiB 102.69 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 106.33 MiB 106.1 MiB 0.2% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Managed heap after settling 9.97 MiB 9.97 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 106.06 MiB 105.79 MiB 0.3% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process CPU time per operation 23.08 µs/op 22.54 µs/op 2.4% less time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 106.59 MiB 106.32 MiB 0.3% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocated memory per operation (estimated) 17.12 KiB/op 17.44 KiB/op 1.9% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Time per operation 22.69 µs/op 22.69 µs/op <0.1% more time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Idle process memory (RSS) 103.23 MiB 103.26 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 113.68 MiB 114.12 MiB 0.4% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Managed heap after settling 9.98 MiB 9.98 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 109.54 MiB 110.06 MiB 0.5% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process CPU time per operation 33.56 µs/op 33.23 µs/op 1% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 116.44 MiB 116.9 MiB 0.4% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocated memory per operation (estimated) 9.94 KiB/op 9.71 KiB/op 2.3% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Time per operation 13.32 µs/op 13.17 µs/op 1.1% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Idle process memory (RSS) 102.53 MiB 102.51 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 106.82 MiB 106.9 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Managed heap after settling 10.05 MiB 10.05 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 106.9 MiB 106.68 MiB 0.2% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process CPU time per operation 24.56 µs/op 24.2 µs/op 1.5% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 106.84 MiB 106.9 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocated memory per operation (estimated) 18.24 KiB/op 17.35 KiB/op 4.9% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Time per operation 20.07 µs/op 19.39 µs/op 3.4% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Idle process memory (RSS) 102.74 MiB 102.92 MiB 0.2% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 109.62 MiB 109.38 MiB 0.2% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Managed heap after settling 10.06 MiB 10.06 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 108.43 MiB 108.12 MiB 0.3% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process CPU time per operation 31.04 µs/op 30.79 µs/op 0.8% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 109.65 MiB 109.38 MiB 0.2% less memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Allocated memory per operation (estimated) 8.52 KiB/op 8.65 KiB/op 1.6% more memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Time per operation 9.33 µs/op 8.95 µs/op 4.1% less time Not flagged
Queue an event (32-byte payload)
Shard: capture
Process CPU time per operation 18.73 µs/op 18.11 µs/op 3.3% less time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Allocated memory per operation (estimated) 8.53 KiB/op 8.39 KiB/op 1.7% less memory Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Time per operation 8.49 µs/op 8.69 µs/op 2.5% more time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Process CPU time per operation 16.97 µs/op 17.02 µs/op 0.3% more time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 132.73 KiB/op 133.65 KiB/op 0.7% more memory Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Time per operation 183.18 µs/op 181.28 µs/op 1% less time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Process CPU time per operation 306.39 µs/op 306.74 µs/op 0.1% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 1.23 MiB/op 1.22 MiB/op 0.7% less memory Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Time per operation 1.59 ms/op 1.59 ms/op 0.3% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Process CPU time per operation 2.63 ms/op 2.64 ms/op 0.4% more time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 12.13 MiB/op 12.11 MiB/op 0.2% less memory Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Time per operation 16.67 ms/op 17.69 ms/op 6.1% more time Worth reviewing: observed regression
Evaluate all 1,000 flags
Shard: flags-scaling
Process CPU time per operation 26.08 ms/op 27.12 ms/op 4% more time Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Idle process memory (RSS) 285.94 MiB 286.31 MiB 0.1% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Observed peak process memory (RSS) 295.49 MiB 296.28 MiB 0.3% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Managed heap after settling 9.62 MiB 9.62 MiB <0.1% less memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Process memory after settling (RSS) 295.41 MiB 296.21 MiB 0.3% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Lifetime peak process memory (RSS) 295.66 MiB 296.28 MiB 0.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Idle process memory (RSS) 222.33 MiB 224.92 MiB 1.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Observed peak process memory (RSS) 239.33 MiB 239.4 MiB <0.1% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Managed heap after settling 11.63 MiB 11.63 MiB <0.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Process memory after settling (RSS) 234.79 MiB 234.72 MiB <0.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Lifetime peak process memory (RSS) 240.77 MiB 241.07 MiB 0.1% more memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocated memory per operation (estimated) 17.28 KiB/op 17.28 KiB/op <0.1% less memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Time per operation 41.31 µs/op 40.79 µs/op 1.3% less time Not flagged
Evaluate one boolean flag
Shard: flags-single
Process CPU time per operation 45 µs/op 43.9 µs/op 2.4% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 14.12 KiB/op 13.95 KiB/op 1.2% less memory Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Time per operation 22.31 µs/op 21.77 µs/op 2.4% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Process CPU time per operation 26.92 µs/op 26.46 µs/op 1.7% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocated memory per operation (estimated) 17.67 KiB/op 17.65 KiB/op 0.1% less memory Not flagged
Evaluate a flag (early match)
Shard: flags-single
Time per operation 22.62 µs/op 23.25 µs/op 2.8% more time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Process CPU time per operation 26.91 µs/op 27.19 µs/op 1.1% more time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocated memory per operation (estimated) 22.49 KiB/op 22.45 KiB/op 0.2% less memory Not flagged
Evaluate a flag (late match)
Shard: flags-single
Time per operation 23.26 µs/op 23.42 µs/op 0.7% more time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Process CPU time per operation 27.12 µs/op 27.54 µs/op 1.6% more time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocated memory per operation (estimated) 17.01 KiB/op 16.92 KiB/op 0.5% less memory Not flagged
Evaluate a flag (no match)
Shard: flags-single
Time per operation 3.05 µs/op 3.09 µs/op 1.2% more time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Process CPU time per operation 6.1 µs/op 6.14 µs/op 0.6% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 12.75 KiB/op 12.89 KiB/op 1.1% more memory Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Time per operation 21.87 µs/op 21.9 µs/op 0.1% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Process CPU time per operation 25.79 µs/op 25.77 µs/op <0.1% less time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 18.1 KiB/op 18.25 KiB/op 0.8% more memory Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Time per operation 42.19 µs/op 41.75 µs/op 1% less time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Process CPU time per operation 46.13 µs/op 45.73 µs/op 0.9% less time Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocated memory per operation (estimated) 14.39 KiB/op 14.16 KiB/op 1.6% less memory Not flagged
Evaluate a rollout flag
Shard: flags-single
Time per operation 31.37 µs/op 31.6 µs/op 0.8% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Process CPU time per operation 34.75 µs/op 35.04 µs/op 0.8% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocated memory per operation (estimated) 17.38 KiB/op 17.41 KiB/op 0.2% more memory Not flagged
Evaluate a multivariate flag
Shard: flags-single
Time per operation 41.16 µs/op 41.33 µs/op 0.4% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Process CPU time per operation 44.96 µs/op 45.6 µs/op 1.4% more time Not flagged

Values are medians of paired measurements. Not flagged does not mean unchanged. Unavailable means a measurement could not be provided, not zero. Estimated allocation measurements retain their instrumentation uncertainty. RSS measures the whole process, not just SDK allocations; observed peaks are sampled checkpoints. Paired ranges in the diagnostics describe variability, not confidence intervals.

Each shard uses its own runner; compare Before PR against With PR within a row, not across shards.

Compared eb11f00d30c734d2de3a5f71aa7d814343588a9f → 37a1cd611f1087d43e41b0caf708e9afb88db1a3.

Full report and diagnostics

102 metric rows; 102 rows shown in all results. Omitted or shortened data remain in comparison.json, including per-runner environments, resource limits, measurement methods, samples and checkpoints.

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