fix(eventcounter): deduplicate events by ID in BigQuery count queries - #2738
Merged
Conversation
BigQuery has no primary keys, so an at-least-once Pub/Sub redelivery after a successful append stores the same event twice. The MySQL and Postgres event tables reject duplicates via PRIMARY KEY (id), but the BigQuery count queries trusted row uniqueness: - evaluation_count.sql used COUNT(id) for the event total, so a duplicated evaluation event inflated evaluationTotal forever. - goal_count.sql aggregated COUNT(id) and SUM(value) per user directly over the raw table, so duplicates inflated goalTotal and the per-user value sums that feed the Bayesian value-metric analysis. User counts (COUNT(DISTINCT user_id)) and therefore the CVR analysis were already duplicate-tolerant. Deduplicate by event ID: COUNT(DISTINCT id) for the evaluation total, and a DISTINCT-by-ID CTE in the goal query before any aggregation. Each legitimate event has its own unique ID, so repeat evaluations/goals by the same user are still counted. This also retroactively corrects duplicates already stored in the tables. This is also what made TestGrpcExperimentResult hang in the dev cluster: the test waits for exact event counts, and a single duplicated event kept the count above the expected total forever. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes BigQuery-backed event counting to be resilient to physically duplicated rows caused by at-least-once Pub/Sub redelivery, aligning BigQuery behavior with MySQL/Postgres tables that reject duplicates via primary keys.
Changes:
- BigQuery evaluation counts: switch from
COUNT(id)toCOUNT(DISTINCT id)to avoid inflated totals from duplicate rows. - BigQuery goal counts: introduce a
deduped_eventsCTE (SELECT DISTINCT ...) before aggregations so duplicates don’t inflate event totals or value-metric inputs.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/eventcounter/storage/v2/dwh_database/bigquery/sql/goal_count.sql | Adds a deduplication CTE before per-user aggregation to prevent duplicate-row inflation of goal counts and value sums. |
| pkg/eventcounter/storage/v2/dwh_database/bigquery/sql/evaluation_count.sql | Counts distinct event IDs for totals to prevent duplicate-row inflation in evaluation totals. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
DISTINCT over the selected columns collapses exact duplicate rows; it is not literally "one row per ID" if non-identical rows ever shared an ID. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
BigQuery has no primary keys, so an at-least-once Pub/Sub redelivery after a
successful append stores the same event twice. The MySQL and Postgres event
tables reject duplicates via
PRIMARY KEY (id), but the BigQuery countqueries silently trusted row uniqueness — a guarantee BigQuery doesn't
provide:
evaluation_count.sqlusedCOUNT(id)for the event total, so oneduplicated evaluation event inflated
evaluationTotalforever.goal_count.sqlaggregatedCOUNT(id)andSUM(value)per user directlyover the raw table, so duplicates inflated
goalTotaland the per-uservalue sums that feed the Bayesian value-metric analysis
(
goalValueTotal/ mean / variance).User counts already used
COUNT(DISTINCT user_id), so unique-user metricsand the CVR analysis were unaffected.
This is also the likely cause of
TestGrpcExperimentResulthanging in thedev cluster: the test waits for exact event counts, and a single duplicated
event keeps the count above the expected total forever (user counts show
50/50 while an event count is stuck at 51).
What
evaluation_count.sql:COUNT(id)→COUNT(DISTINCT id).goal_count.sql: added adeduped_eventsCTE (SELECT DISTINCT id, ...)before any aggregation; the winsorization and final aggregation stages are
unchanged.
The metric semantics are preserved: each legitimate event has its own unique
ID, so repeat evaluations/goals by the same user still count toward the
totals. Only physically duplicated rows are collapsed. Because the dedup is
at read time, it also retroactively corrects duplicates already sitting in
the tables.
Performance
idand the filters are identical.
filtered rows), but the filter is already narrow (one feature/goal +
version + experiment window) and the queries run in the batch calculator,
not on a user-facing path. The queries already pay for an exact
COUNT(DISTINCT user_id), so this adds no new class of operation.Notes
duplicate rows impossible at write time.
test/eventcounter-diagnose-stuck-counts,which makes the e2e wait loop print all eight counters and fail fast on
over-counts.