diff --git a/VERSION b/VERSION index 437459c..73462a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5.0 +2.5.1 diff --git a/metrics/legacy_matomo/opensearch_actions.py b/metrics/legacy_matomo/opensearch_actions.py index 7adaa95..847d321 100644 --- a/metrics/legacy_matomo/opensearch_actions.py +++ b/metrics/legacy_matomo/opensearch_actions.py @@ -59,7 +59,10 @@ def increment = params.document.containsKey(field) ? params.document[field] : 0; ctx._source.applied_day_masks = params.empty_day_masks; } for (int index = 0; index < params.day_masks.size(); index++) { - if ((ctx._source.applied_day_masks[index] & params.day_masks[index]) != 0) { + // JSON zeroes may deserialize as Integer; force long before using high bits. + long currentMask = ((Number) ctx._source.applied_day_masks[index]).longValue(); + long incomingMask = ((Number) params.day_masks[index]).longValue(); + if ((currentMask & incomingMask) != 0) { throw new IllegalStateException('Historical migration overlaps an applied analytics day'); } } @@ -78,7 +81,9 @@ def increment = params.document.containsKey(field) ? params.document[field] : 0; ctx._source[field] = currentValue + increment; } for (int index = 0; index < params.day_masks.size(); index++) { - ctx._source.applied_day_masks[index] |= params.day_masks[index]; + long currentMask = ((Number) ctx._source.applied_day_masks[index]).longValue(); + long incomingMask = ((Number) params.day_masks[index]).longValue(); + ctx._source.applied_day_masks[index] = currentMask | incomingMask; } ctx._source.applied_migrations.add(params.migration_id); """ diff --git a/metrics/tests/services/test_legacy_matomo_importer.py b/metrics/tests/services/test_legacy_matomo_importer.py index 4ff250c..4c022ef 100644 --- a/metrics/tests/services/test_legacy_matomo_importer.py +++ b/metrics/tests/services/test_legacy_matomo_importer.py @@ -50,6 +50,22 @@ def test_analytics_action_builds_all_day_masks(self): day_masks[august_last_offset // 63] & (1 << (august_last_offset % 63)) ) + def test_analytics_action_preserves_masks_larger_than_32_bits(self): + action = build_analytics_increment_action( + "usage_yearly_analytics_scl_2025", + "key", + METRICS, + "migration-with-hash", + ["2025-09-01", "2025-09-30"], + ) + + day_masks = action["script"]["params"]["day_masks"] + script = action["script"]["source"] + + self.assertGreater(max(day_masks), (1 << 31) - 1) + self.assertIn("((Number) params.day_masks[index]).longValue()", script) + self.assertNotIn("applied_day_masks[index] |=", script) + @override_settings(OPENSEARCH_INDEX_NAME="usage") def test_preflight_builds_yearly_targets_without_writes(self): collection = SimpleNamespace(