Skip to content

fix(eventcounter): deduplicate events by ID in BigQuery count queries - #2738

Merged
cre8ivejp merged 2 commits into
mainfrom
fix/bq-dedup-event-counts
Jul 31, 2026
Merged

fix(eventcounter): deduplicate events by ID in BigQuery count queries#2738
cre8ivejp merged 2 commits into
mainfrom
fix/bq-dedup-event-counts

Conversation

@cre8ivejp

Copy link
Copy Markdown
Member

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 count
queries silently trusted row uniqueness — a guarantee BigQuery doesn't
provide:

  • evaluation_count.sql used COUNT(id) for the event total, so one
    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
    (goalValueTotal / mean / variance).

User counts already used COUNT(DISTINCT user_id), so unique-user metrics
and the CVR analysis were unaffected.

This is also the likely cause of TestGrpcExperimentResult hanging in the
dev 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 a deduped_events CTE (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

  • Bytes scanned (billing) are unchanged — both queries already read id
    and the filters are identical.
  • Compute increases slightly (one exact-distinct / dedup pass over the
    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

  • MySQL and Postgres queries are untouched: their primary keys make
    duplicate rows impossible at write time.
  • Complements the diagnostic branch test/eventcounter-diagnose-stuck-counts,
    which makes the e2e wait loop print all eight counters and fail fast on
    over-counts.

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>
Copilot AI review requested due to automatic review settings July 30, 2026 15:20

Copilot AI left a comment

Copy link
Copy Markdown

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 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) to COUNT(DISTINCT id) to avoid inflated totals from duplicate rows.
  • BigQuery goal counts: introduce a deduped_events CTE (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.

Comment thread pkg/eventcounter/storage/v2/dwh_database/bigquery/sql/goal_count.sql Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

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

@cre8ivejp
cre8ivejp marked this pull request as ready for review July 30, 2026 15:29
@cre8ivejp
cre8ivejp requested review from hvn2k1 and t-kikuc as code owners July 30, 2026 15:29
@cre8ivejp
cre8ivejp requested a review from Ubisoft-potato July 30, 2026 15:29

@Ubisoft-potato Ubisoft-potato left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM! 🚀

@cre8ivejp
cre8ivejp merged commit 479cd30 into main Jul 31, 2026
12 checks passed
@cre8ivejp
cre8ivejp deleted the fix/bq-dedup-event-counts branch July 31, 2026 02:19
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.

3 participants